From cf791d86530e70b1325b1be175b98941161570db Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Mon, 8 Jun 2026 12:08:41 +0200 Subject: [PATCH 1/2] fix(realunit): offer NewRegistration empty form for first-time users instead of KycRequired MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getRegistrationInfo dead-ended onboarding for any wallet without pre-fillable KYC data: it returned KycRequired, which the client surfaces as a terminal "complete your identity verification first" failure — yet that is exactly the first-time user the registration flow exists to onboard. completeRegistration requires only KYC Level 10 (email) and persists manually-entered data when no prior data exists, so the form must be offered, not withheld. Return NewRegistration with userData omitted (empty form) when neither a registration step nor pre-fill data exist; the existing prefill path is unchanged. KycRequired is kept as a reserved, no-longer-emitted contract value so clients retain explicit handling. Regression introduced in #3782. --- .../__tests__/realunit.service.spec.ts | 4 +-- .../controllers/realunit.controller.ts | 2 +- .../realunit/dto/realunit-registration.dto.ts | 6 ++++- .../supporting/realunit/realunit.service.ts | 26 +++++++------------ 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/subdomains/supporting/realunit/__tests__/realunit.service.spec.ts b/src/subdomains/supporting/realunit/__tests__/realunit.service.spec.ts index 592a1fb9b6..6c88ab96e0 100644 --- a/src/subdomains/supporting/realunit/__tests__/realunit.service.spec.ts +++ b/src/subdomains/supporting/realunit/__tests__/realunit.service.spec.ts @@ -577,7 +577,7 @@ describe('RealUnitService', () => { expect(status.userData!.kycData.lastName).toBe('Mustermann'); }); - it('returns state=KYC_REQUIRED when no step exists and no KYC data is present', () => { + it('returns state=NEW_REGISTRATION with no userData when no step exists and no KYC data is present (first-time user gets an empty form)', () => { const userData = { firstname: null, surname: null, @@ -586,7 +586,7 @@ describe('RealUnitService', () => { const status = service.getRegistrationInfo(userData, walletAddress); - expect(status.state).toBe(RealUnitRegistrationState.KYC_REQUIRED); + expect(status.state).toBe(RealUnitRegistrationState.NEW_REGISTRATION); expect(status.isRegistered).toBe(false); expect(status.userData).toBeUndefined(); }); diff --git a/src/subdomains/supporting/realunit/controllers/realunit.controller.ts b/src/subdomains/supporting/realunit/controllers/realunit.controller.ts index ea66d6c19b..b4c3e4b663 100644 --- a/src/subdomains/supporting/realunit/controllers/realunit.controller.ts +++ b/src/subdomains/supporting/realunit/controllers/realunit.controller.ts @@ -612,7 +612,7 @@ export class RealUnitController { @ApiOperation({ summary: 'Get RealUnit registration info for the connected wallet', description: - 'Returns the action the client should take to RealUnit-register the connected wallet (`state`), the registration data to pre-fill or display (`userData`), and a legacy `isRegistered` flag. Drives the registration UX: client routes on `state` (AlreadyRegistered / AddWallet / NewRegistration / KycRequired) without inferring it locally.', + 'Returns the action the client should take to RealUnit-register the connected wallet (`state`), the registration data to pre-fill or display (`userData`), and a legacy `isRegistered` flag. Drives the registration UX: client routes on `state` (AlreadyRegistered / AddWallet / NewRegistration) without inferring it locally.', }) @ApiOkResponse({ type: RealUnitRegistrationInfoDto }) async getRegistrationInfo(@GetJwt() jwt: JwtPayload): Promise { diff --git a/src/subdomains/supporting/realunit/dto/realunit-registration.dto.ts b/src/subdomains/supporting/realunit/dto/realunit-registration.dto.ts index 4e4d5e5522..5356d52da4 100644 --- a/src/subdomains/supporting/realunit/dto/realunit-registration.dto.ts +++ b/src/subdomains/supporting/realunit/dto/realunit-registration.dto.ts @@ -240,6 +240,10 @@ export enum RealUnitRegistrationState { ALREADY_REGISTERED = 'AlreadyRegistered', ADD_WALLET = 'AddWallet', NEW_REGISTRATION = 'NewRegistration', + // Reserved, defensive value: `getRegistrationInfo` no longer emits this. First-time users with no + // pre-fillable KYC data get NEW_REGISTRATION (empty form) instead — withholding the registration + // step here used to dead-end onboarding. Kept in the contract so clients retain explicit handling + // should a future "KYC blocked" condition be surfaced. KYC_REQUIRED = 'KycRequired', } @@ -254,7 +258,7 @@ export class RealUnitRegistrationInfoDto { @ApiProperty({ enum: RealUnitRegistrationState, description: - 'Action the client should take for this wallet. `AlreadyRegistered`: no UX needed. `AddWallet`: render a one-tap Add-Wallet flow that submits to POST /register/wallet using the prior signed payload (`userData` is set). `NewRegistration`: render the full registration form pre-filled with `userData`. `KycRequired`: user must complete DFX KYC first (`userData` not set; edge case).', + 'Action the client should take for this wallet. `AlreadyRegistered`: no UX needed. `AddWallet`: render a one-tap Add-Wallet flow that submits to POST /register/wallet using the prior signed payload (`userData` is set). `NewRegistration`: render the full registration form — pre-filled with `userData` when present, otherwise empty for the client to collect every field manually. `KycRequired`: reserved defensive value, not currently emitted by this endpoint.', }) state: RealUnitRegistrationState; diff --git a/src/subdomains/supporting/realunit/realunit.service.ts b/src/subdomains/supporting/realunit/realunit.service.ts index aa42630a50..13029bc276 100644 --- a/src/subdomains/supporting/realunit/realunit.service.ts +++ b/src/subdomains/supporting/realunit/realunit.service.ts @@ -670,11 +670,10 @@ export class RealUnitService { getRegistrationInfo(userData: UserData, walletAddress: string): RealUnitRegistrationInfoDto { const { step, isForCurrentWallet } = this.findRegistrationStep(userData, walletAddress); - // Dispatch to one of four states so the client can route to the right UX without inferring + // Dispatch to one of three states so the client can route to the right UX without inferring // it locally. Order matters: a registration step for the current wallet (ALREADY_REGISTERED) // wins over any other signal; a step for a different wallet drives the one-tap Add-Wallet - // flow (ADD_WALLET); otherwise we pre-fill the full form from existing KYC data when - // available (NEW_REGISTRATION), falling back to KYC_REQUIRED when no usable data exists. + // flow (ADD_WALLET); otherwise this wallet still needs a fresh registration (NEW_REGISTRATION). if (step) { const stepUserData = this.toUserDataDto(step); const state = isForCurrentWallet @@ -687,21 +686,16 @@ export class RealUnitService { }; } - // No step exists. Pre-fill from DFX KYC data (firstname/surname guarded by - // toUserDataDtoFromUserData) and fall through to KYC_REQUIRED when that returns undefined. - const prefill = this.toUserDataDtoFromUserData(userData); - if (prefill) { - return { - isRegistered: false, - state: RealUnitRegistrationState.NEW_REGISTRATION, - userData: prefill, - }; - } - + // No step exists: this wallet needs a fresh RealUnit registration. Pre-fill the form from + // existing DFX KYC data when we have verified personal data (firstname/surname present); + // otherwise return NEW_REGISTRATION without `userData` so the client renders an empty form and + // collects every field manually. `completeRegistration` accepts and persists manually-entered + // data for first-time users — email registration (KYC Level 10) is the only prerequisite — so + // this branch must not dead-end onboarding by withholding the registration step. return { isRegistered: false, - state: RealUnitRegistrationState.KYC_REQUIRED, - userData: undefined, + state: RealUnitRegistrationState.NEW_REGISTRATION, + userData: this.toUserDataDtoFromUserData(userData), }; } From 45409ce51d6070578b9f4c7324b57906ddc1d532 Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Mon, 8 Jun 2026 13:36:26 +0200 Subject: [PATCH 2/2] fix(realunit): drop unemitted KycRequired registration state (YAGNI) --- .../supporting/realunit/dto/realunit-registration.dto.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/subdomains/supporting/realunit/dto/realunit-registration.dto.ts b/src/subdomains/supporting/realunit/dto/realunit-registration.dto.ts index 5356d52da4..daf717f1ca 100644 --- a/src/subdomains/supporting/realunit/dto/realunit-registration.dto.ts +++ b/src/subdomains/supporting/realunit/dto/realunit-registration.dto.ts @@ -240,11 +240,6 @@ export enum RealUnitRegistrationState { ALREADY_REGISTERED = 'AlreadyRegistered', ADD_WALLET = 'AddWallet', NEW_REGISTRATION = 'NewRegistration', - // Reserved, defensive value: `getRegistrationInfo` no longer emits this. First-time users with no - // pre-fillable KYC data get NEW_REGISTRATION (empty form) instead — withholding the registration - // step here used to dead-end onboarding. Kept in the contract so clients retain explicit handling - // should a future "KYC blocked" condition be surfaced. - KYC_REQUIRED = 'KycRequired', } export class RealUnitRegistrationInfoDto { @@ -258,7 +253,7 @@ export class RealUnitRegistrationInfoDto { @ApiProperty({ enum: RealUnitRegistrationState, description: - 'Action the client should take for this wallet. `AlreadyRegistered`: no UX needed. `AddWallet`: render a one-tap Add-Wallet flow that submits to POST /register/wallet using the prior signed payload (`userData` is set). `NewRegistration`: render the full registration form — pre-filled with `userData` when present, otherwise empty for the client to collect every field manually. `KycRequired`: reserved defensive value, not currently emitted by this endpoint.', + 'Action the client should take for this wallet. `AlreadyRegistered`: no UX needed. `AddWallet`: render a one-tap Add-Wallet flow that submits to POST /register/wallet using the prior signed payload (`userData` is set). `NewRegistration`: render the full registration form — pre-filled with `userData` when present, otherwise empty for the client to collect every field manually.', }) state: RealUnitRegistrationState;