refactor(signup): drive Fayda through register-with-fayda

Follows the API back to a single endpoint. The page posts { action: 'start' }
to open the attempt, the callback page posts { action: 'verify', ... } to get
the verified identity, and the existing signup submission is unchanged.

Drops the post-signup link call: there is no longer a link endpoint, and the
account is created by the signup endpoint that already owns user creation.
This commit is contained in:
mihretue
2026-08-25 13:01:40 +00:00
parent 933ba41476
commit c4c84abc66
3 changed files with 19 additions and 32 deletions

View File

@@ -63,9 +63,11 @@ export function FaydaCallbackPage() {
}
callbackTrigger({
url: '/auth/fayda/callback',
url: '/auth/register-with-fayda',
method: 'POST',
body: { code, state, transactionToken: request.transactionToken },
// `verify` returns the identity without creating an account — the
// existing signup endpoint still does that.
body: { action: 'verify', code, state, transactionToken: request.transactionToken },
})
.unwrap()
.then((result) => {

View File

@@ -31,7 +31,7 @@ import { z } from 'zod';
import { useNavigate, Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { useApiLazyQuery, useApiMutation } from '@ema-platform/api';
import { useApiMutation } from '@ema-platform/api';
import { useErrorHandler, passwordSchema, PasswordRequirements, phoneNumber, PhoneInput } from '@ema-platform/ui';
import { AuthShell } from '../components/AuthShell';
import { loginSuccess, setUser } from '../store/auth.slice';
@@ -70,13 +70,12 @@ export function SignupPage() {
// Fayda is optional: the form below works exactly as before without it.
const [fayda, setFayda] = useState<FaydaResult | null>(() => faydaSession.peekResult());
const [faydaStarting, setFaydaStarting] = useState(false);
const [authorizeTrigger] = useApiLazyQuery<{
const [startTrigger] = useApiMutation<{
authorizationUrl: string;
state: string;
transactionToken: string;
expiresIn: number;
}>();
const [linkTrigger] = useApiMutation<void>();
const verified = (field: string) => fayda?.verifiedFields.includes(field) ?? false;
const conflicted = (field: string) => fayda?.conflicts.includes(field) ?? false;
@@ -174,7 +173,7 @@ export function SignupPage() {
// are always the applicant's to choose, and Fayda supplies neither.
useEffect(() => {
if (!fayda) return;
const { email, phoneNumber: phone, nameEn, nameAm } = fayda.prefill;
const { email, phoneNumber: phone, nameEn, nameAm } = fayda.identity;
if (email) setValue('email', email);
if (phone) setValue('phoneNumber', phone);
if (nameEn) setValue('nameEn', nameEn);
@@ -185,8 +184,12 @@ export function SignupPage() {
setServerError(null);
setFaydaStarting(true);
try {
const { authorizationUrl, transactionToken, state } = await authorizeTrigger({
url: '/auth/fayda/authorize',
// Same endpoint the registration itself uses; `start` only opens the
// attempt and hands back where to send the user.
const { authorizationUrl, transactionToken, state } = await startTrigger({
url: '/auth/register-with-fayda',
method: 'POST',
body: { action: 'start' },
}).unwrap();
faydaSession.saveRequest({ transactionToken, state });
@@ -231,23 +234,7 @@ export function SignupPage() {
const me = await meTrigger({ url: '/auth/me', method: 'GET' }).unwrap();
dispatch(setUser(me));
// Records the Fayda identity on the account that was just created. The
// registration endpoint is shared platform code and drops fields it does
// not know, so the link has to be a separate call. It is best-effort: the
// account is already usable, and the worst case is that it is not marked
// as Fayda-verified.
if (fayda) {
try {
await linkTrigger({
url: '/auth/fayda/link',
method: 'POST',
body: { verificationToken: fayda.verificationToken },
}).unwrap();
} catch {
/* deliberately ignored — signup already succeeded */
}
faydaSession.clearResult();
}
faydaSession.clearResult();
if (data.isPhoneNumberVerified) {
navigate(loginRedirectPath);

View File

@@ -3,9 +3,9 @@
* survive it lives in sessionStorage: same tab, same origin, gone when the tab
* closes.
*
* Nothing secret is kept here. `transactionToken` and `verificationToken` are
* signed by the API and are useless without it — the PKCE verifier and the
* client secret never leave the backend.
* Nothing secret is kept here. The `transactionToken` is signed by the API and
* useless without it — the PKCE verifier, the nonce and the client key never
* leave the backend.
*/
const REQUEST_KEY = 'fayda:request';
@@ -26,16 +26,14 @@ export interface FaydaPrefill {
address?: string;
}
/** Shape of `POST /auth/register-with-fayda` with `action: "verify"`. */
export interface FaydaResult {
prefill: FaydaPrefill;
/** Always true when the API returned a result at all. */
identity: FaydaPrefill;
faydaVerified: boolean;
/** Signup fields Fayda vouched for. */
verifiedFields: string[];
/** Prefilled fields already taken by another account. */
conflicts: string[];
/** Posted to /auth/fayda/link once the account exists. */
verificationToken: string;
}
// Private browsing and locked-down browsers can throw on access, and a failure