diff --git a/apps/e2e/src/seafarer-registration.spec.ts b/apps/e2e/src/seafarer-registration.spec.ts
index 2f03ff5ed..b68b7fc07 100644
--- a/apps/e2e/src/seafarer-registration.spec.ts
+++ b/apps/e2e/src/seafarer-registration.spec.ts
@@ -23,7 +23,12 @@ import { approveRegistration, runWorkflow } from './support/workflow';
*/
/**
- * Fills the fields `RequireSeafarerProfile` refuses to open the wizard without.
+ * Fills the profile the seafarer wizard prefills its Identity Details step
+ * from.
+ *
+ * No longer a precondition for reaching the wizard — that redirect is gone and
+ * the step collects these itself — but a populated profile is the returning
+ * applicant's case, and it is the prefill that keeps them from retyping.
*
* They are split across two tabs, and every tab's panel is in the DOM whether
* or not it is showing — so each one has to be selected before its inputs can
diff --git a/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx b/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx
index 39cbdd9d3..5352f520c 100644
--- a/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx
+++ b/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx
@@ -227,12 +227,17 @@ export function LicenseApplicationPage() {
detail?.application?.formData,
]);
- // Generic fill for every field the config marks `readOnly` with a
- // `source` — e.g. seafarer registration's read-only Identity Details step,
- // which shows what's already on the profile instead of asking again.
- // `readOnly` fields are never sent by the applicant and the server skips
- // them at validation, so this is display-only; the profile itself is what
- // an edit has to go through.
+ // Generic fill for every field the config gives a `source` — the profile
+ // value the applicant would otherwise retype. Seafarer registration's
+ // Identity Details step is the case that drives this: it collects name,
+ // gender, DOB and national ID *in the wizard* rather than sending the
+ // applicant to `/profile` first, so those fields are editable and this is a
+ // prefill, not a display.
+ //
+ // Editable sourced fields are filled only while still blank. Re-running
+ // this effect (a refetched profile, a saved draft) must not overwrite what
+ // the applicant has since typed — for a `readOnly` field the profile stays
+ // authoritative, so those keep tracking it.
useEffect(() => {
if (!profile || !config) return;
const context = { user: profile.user, profile };
@@ -242,10 +247,14 @@ export function LicenseApplicationPage() {
const next = { ...prev };
for (const section of config.licenseType.formSchema.sections) {
for (const field of section.fields) {
- if (!field.readOnly || !field.source) continue;
+ if (!field.source) continue;
+ const current = next[section.key]?.[field.key];
+ const untouched =
+ current === undefined || current === null || current === "";
+ if (!field.readOnly && !untouched) continue;
const value = readSourcePath(context, field.source);
if (value === undefined || value === null || value === "") continue;
- if (next[section.key]?.[field.key] === value) continue;
+ if (current === value) continue;
next[section.key] = { ...next[section.key], [field.key]: value };
changed = true;
}
diff --git a/apps/portal/src/app/features/profile/components/RequireSeafarerProfile.tsx b/apps/portal/src/app/features/profile/components/RequireSeafarerProfile.tsx
index 02cca1bf0..5856d706d 100644
--- a/apps/portal/src/app/features/profile/components/RequireSeafarerProfile.tsx
+++ b/apps/portal/src/app/features/profile/components/RequireSeafarerProfile.tsx
@@ -1,24 +1,14 @@
-import { useEffect } from 'react';
import { Center, Loader } from '@mantine/core';
-import { Navigate, useLocation, useParams } from 'react-router-dom';
-import { useTranslation } from 'react-i18next';
-import { notify } from '@ema-platform/ui';
-import {
- PROFILE_FIELD_SECTION,
- useCurrentProfile,
- type ProfileRequirement,
-} from '@ema-platform/auth';
+import { Navigate, useParams } from 'react-router-dom';
+import { useCurrentProfile, type ProfileRequirement } from '@ema-platform/auth';
/**
- * Seafarer registration is filled in from the profile (nationality, ID,
- * names, contact details) — the server refuses an application missing them,
- * so they're asked for up front instead of at submit time.
+ * The identity the seafarer wizard needs before it can produce a registration.
*
- * Only the fields the Personal, Maritime Profile and Address tabs actually
- * mark required — matches `profileSchema` / `addressSchema`, so the gate is
- * always satisfiable by finishing those tabs and never blocks on an optional
- * field (place of birth, region/city/woreda, emergency contact) the forms
- * don't star.
+ * No longer a gate on opening the wizard: the Identity Details step collects
+ * these itself, so an applicant with an empty profile starts in registration
+ * rather than being sent to `/profile` to prepare for it. Kept because
+ * `ProfilePage` still reads it to show what a seafarer registration will need.
*/
export const SEAFARER_PROFILE_REQUIREMENT: ProfileRequirement = {
fields: [
@@ -42,35 +32,26 @@ export const SEAFARER_PROFILE_REQUIREMENT: ProfileRequirement = {
const REGISTRATION_TYPE_KEY = 'SEAFARER_REGISTRATION';
/**
- * Sends an applicant with an incomplete profile to `/profile` before they can
- * reach seafarer registration. Wraps `/seafarer-registration` directly and
- * `/licensing/:typeCode/apply` when `typeCode` is the seafarer type — the
- * latter is the shared wizard route every licence type renders through, so
- * without it the gate is a decoration a deep link skips.
+ * Guards the seafarer wizard against the one case it cannot serve: an
+ * applicant who already holds a seafarer number.
*
- * Fires before the wizard starts, not mid-application, so nothing is lost —
- * unlike the case `ProfileRequirementGate`'s doc comment warns against
- * (mid-flow redirects on the old, deleted setup wizard).
+ * It deliberately does *not* gate on profile completeness any more. Selecting
+ * Seafarer Registration now opens the wizard, and the Identity Details step
+ * collects name, gender, DOB, marital status, nationality and national ID
+ * itself — an empty profile is a thing the wizard fills, not a reason to be
+ * sent away from it. Those answers reach the profile when a reviewer approves
+ * the registration (`CompletionEffectService.registerSeafarer`).
+ *
+ * Wraps `/seafarer-registration` directly and `/licensing/:typeCode/apply`
+ * when `typeCode` is the seafarer type — the latter is the shared wizard route
+ * every licence type renders through, so without it a deep link skips this.
*/
export function RequireSeafarerProfile({ children }: { children: React.ReactNode }) {
- const { t } = useTranslation();
const { typeCode } = useParams();
- const { pathname } = useLocation();
- const { isLoading, isFetching, error, gapsFor, profile } = useCurrentProfile();
+ const { isLoading, error, profile } = useCurrentProfile();
- // Shared wizard route — only the seafarer type is gated here.
+ // Shared wizard route — only the seafarer type is checked here.
const gated = !typeCode || typeCode === REGISTRATION_TYPE_KEY;
- const gaps = gated ? gapsFor(SEAFARER_PROFILE_REQUIREMENT) : [];
- const redirecting = gated && !isLoading && !error && !isFetching && gaps.length > 0;
-
- useEffect(() => {
- if (!redirecting) return;
- const fields = gaps.map((field) => t(`profileFields.${field}`, field)).join(', ');
- notify.info(t('profileGate.seafarerRedirect', { fields }));
- // Fire once per redirect, not on every render while gaps/gapsFor are
- // recreated — the toast content is captured at the moment it fires.
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [redirecting, pathname]);
if (!gated) return <>{children}>;
@@ -91,24 +72,10 @@ export function RequireSeafarerProfile({ children }: { children: React.ReactNode
return