@dloizides/auth-web 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0 (2026-05-31)
4
+
5
+ Additive release for the auth-UX reuse roll-out (feature-reuse-roadmap #1).
6
+ Promotes the "Forgot password?" modal body the product apps each hand-rolled
7
+ into a single shared, **react-query-free** export so katalogos / erevna / kefi
8
+ stop duplicating it. No breaking changes; every 1.2.x export is unchanged.
9
+
10
+ ### Added
11
+
12
+ - **`<ForgotPasswordFields>`** — the embedded, themeable "request a reset link"
13
+ body, sized to live inside an app-owned modal shell (no screen/card wrapper,
14
+ no title — the host modal owns those). Adds the two modal affordances a
15
+ screen-shaped form lacks: a `Cancel` button beside submit and a `Close` button
16
+ on the success state, each rendered only when its handler is supplied. Clears
17
+ itself when `visible` flips to `false` so reopening starts fresh.
18
+ - **`useForgotPasswordSubmit`** — the headless, **react-query-free** forgot-
19
+ password logic: a plain `useState` + `client.forgotPassword(...)` promise
20
+ chain that runs on a provider-less login route (where the existing
21
+ `useBffForgotPassword` `useMutation` crashes with "No QueryClient set"). Email
22
+ validation gating via `canSubmit`, anti-enumeration success, `hasNetworkError`,
23
+ and `reset()`. Also exports the `isValidForgotPasswordEmail` helper.
24
+ - **`ForgotPasswordFieldsLabels`** + **`DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS`** —
25
+ the forgot-password label bag plus the modal `cancel` / `close` strings.
26
+ - New `AuthTestIds` entries `forgotPasswordCancelButton` /
27
+ `forgotPasswordCloseButton`.
28
+
29
+ ### Notes
30
+
31
+ - The existing screen-shaped `<ForgotPasswordForm>` (react-query-based) is
32
+ unchanged. A future cleanup may re-base it on `useForgotPasswordSubmit` to drop
33
+ its react-query coupling; deferred to keep this release additive.
34
+
3
35
  ## 1.2.0 (2026-05-22)
4
36
 
5
37
  Additive release for Phase 3d of the unified-auth plan — the event-scoped PIN
package/dist/index.d.mts CHANGED
@@ -28,6 +28,9 @@ interface LoginFormLabels {
28
28
  submitting: string;
29
29
  /** The "Forgot password?" link text. */
30
30
  forgotPassword: string;
31
+ /** The "Create account" link shown below the submit button when an
32
+ * `onSignUp` handler is supplied. Omit the handler to hide the link. */
33
+ signUp: string;
31
34
  /** Generic credentials-rejected message. */
32
35
  invalidCredentials: string;
33
36
  /** Shown when one or both fields are empty on submit. */
@@ -49,6 +52,17 @@ interface ForgotPasswordFormLabels {
49
52
  /** Shown when the entered value is not a valid email. */
50
53
  invalidEmail: string;
51
54
  }
55
+ /**
56
+ * Strings rendered by `<ForgotPasswordFields>` — the embedded modal body. It is
57
+ * the forgot-password form plus the two modal affordances (`cancel` alongside
58
+ * submit, `close` on the success state) that a screen-shaped form has no need of.
59
+ */
60
+ interface ForgotPasswordFieldsLabels extends ForgotPasswordFormLabels {
61
+ /** The "cancel" button shown alongside submit. */
62
+ cancel: string;
63
+ /** The "close" button shown on the success state. */
64
+ close: string;
65
+ }
52
66
  /** Strings rendered by `<OtpForm>` — the two-step email-OTP login form. */
53
67
  interface OtpFormLabels {
54
68
  /** Title shown on the request-a-code step. */
@@ -124,6 +138,7 @@ interface ResetPasswordFormLabels {
124
138
  }
125
139
  declare const DEFAULT_LOGIN_LABELS: LoginFormLabels;
126
140
  declare const DEFAULT_FORGOT_PASSWORD_LABELS: ForgotPasswordFormLabels;
141
+ declare const DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS: ForgotPasswordFieldsLabels;
127
142
  declare const DEFAULT_OTP_LABELS: OtpFormLabels;
128
143
  declare const DEFAULT_PIN_LABELS: PinFormLabels;
129
144
  declare const DEFAULT_RESET_PASSWORD_LABELS: ResetPasswordFormLabels;
@@ -233,11 +248,18 @@ interface LoginFormProps {
233
248
  onSuccess: (user: BffUser) => void;
234
249
  /** Called when the user taps the "Forgot password?" link. Omit to hide the link. */
235
250
  onForgotPassword?: () => void;
251
+ /**
252
+ * Called when the user taps the "Create account" link. Omit to hide the link
253
+ * (the registration UI then has to be reached some other way — direct nav,
254
+ * marketing CTA, etc.). The consuming app decides where the link routes;
255
+ * `<LoginForm>` just surfaces the entry point.
256
+ */
257
+ onSignUp?: () => void;
236
258
  /** Prefix applied to every `testID` so multiple forms can share a screen. */
237
259
  testIdPrefix?: string;
238
260
  }
239
261
  /** Themeable password-login form built on `useBffAuth`. */
240
- declare function LoginForm({ client, theme: themeProp, labels: labelsProp, onSuccess, onForgotPassword, testIdPrefix, }: Readonly<LoginFormProps>): ReactElement;
262
+ declare function LoginForm({ client, theme: themeProp, labels: labelsProp, onSuccess, onForgotPassword, onSignUp, testIdPrefix, }: Readonly<LoginFormProps>): ReactElement;
241
263
 
242
264
  /**
243
265
  * `<ForgotPasswordForm>` — the ready-made, themeable "request a reset link"
@@ -274,6 +296,56 @@ interface ForgotPasswordFormProps {
274
296
  /** Themeable "request a reset link" form built on `useBffForgotPassword`. */
275
297
  declare function ForgotPasswordForm({ client, theme: themeProp, labels: labelsProp, resetUrlTemplate, onSuccess, testIdPrefix, }: Readonly<ForgotPasswordFormProps>): ReactElement;
276
298
 
299
+ /**
300
+ * `<ForgotPasswordFields>` — the embedded, themeable "request a reset link"
301
+ * body, sized to live INSIDE an app-owned modal shell (not a full screen).
302
+ *
303
+ * The product apps each show "Forgot password?" as a modal launched from their
304
+ * login route, wrapping their own modal chrome (title bar + close affordance)
305
+ * around an identical email field + submit/cancel body. This is that body,
306
+ * promoted into the package so the three apps stop hand-rolling it.
307
+ *
308
+ * Built on the react-query-free `useForgotPasswordSubmit` hook, so it renders +
309
+ * submits on a provider-less login route (where `useMutation` would crash). The
310
+ * backend is anti-enumeration, so a successful request swaps to a generic
311
+ * confirmation that never reveals whether the address is registered.
312
+ *
313
+ * Differs from the screen-shaped `<ForgotPasswordForm>`: no outer screen/card
314
+ * wrapper and no title (the host modal owns those), plus two modal affordances —
315
+ * a `Cancel` button beside submit and a `Close` button on the success state —
316
+ * each rendered only when its handler is supplied.
317
+ */
318
+
319
+ interface ForgotPasswordFieldsProps {
320
+ /** The same-origin BFF client (build it with `createBffAuthClient`). */
321
+ client: BffAuthClient;
322
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
323
+ theme?: AuthTheme;
324
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
325
+ labels?: Partial<ForgotPasswordFieldsLabels>;
326
+ /**
327
+ * Full URL with a `{token}` placeholder, forwarded to the backend so it can
328
+ * build the reset-email link without hardcoding a frontend host.
329
+ */
330
+ resetUrlTemplate?: string;
331
+ /**
332
+ * The host modal's open state. When it flips to `false` the body clears
333
+ * itself, so reopening shows a fresh form regardless of which affordance
334
+ * closed it. Defaults to `true` for non-modal embedding.
335
+ */
336
+ visible?: boolean;
337
+ /** Called once the request succeeds (the generic-confirmation state). */
338
+ onSuccess?: () => void;
339
+ /** When supplied, renders a Cancel button beside submit. */
340
+ onCancel?: () => void;
341
+ /** When supplied, renders a Close button on the success state. */
342
+ onClose?: () => void;
343
+ /** Prefix applied to every `testID`. */
344
+ testIdPrefix?: string;
345
+ }
346
+ /** Embedded "request a reset link" body for an app-owned modal. */
347
+ declare function ForgotPasswordFields({ client, theme: themeProp, labels: labelsProp, resetUrlTemplate, visible, onSuccess, onCancel, onClose, testIdPrefix, }: Readonly<ForgotPasswordFieldsProps>): ReactElement;
348
+
277
349
  /**
278
350
  * `<ResetPasswordForm>` — the ready-made, themeable "choose a new password"
279
351
  * form.
@@ -423,10 +495,13 @@ declare const AuthTestIds: {
423
495
  readonly loginPasswordInput: "auth-login-password";
424
496
  readonly loginSubmitButton: "auth-login-submit";
425
497
  readonly loginForgotLink: "auth-login-forgot-link";
498
+ readonly loginSignUpLink: "auth-login-signup-link";
426
499
  readonly loginError: "auth-login-error";
427
500
  readonly forgotPasswordForm: "auth-forgot-form";
428
501
  readonly forgotPasswordEmailInput: "auth-forgot-email";
429
502
  readonly forgotPasswordSubmitButton: "auth-forgot-submit";
503
+ readonly forgotPasswordCancelButton: "auth-forgot-cancel";
504
+ readonly forgotPasswordCloseButton: "auth-forgot-close";
430
505
  readonly forgotPasswordError: "auth-forgot-error";
431
506
  readonly forgotPasswordSuccess: "auth-forgot-success";
432
507
  readonly resetPasswordForm: "auth-reset-form";
@@ -579,6 +654,38 @@ interface UseResetPasswordFormResult {
579
654
  */
580
655
  declare function useResetPasswordForm({ client, token, onSuccess, }: UseResetPasswordFormArgs): UseResetPasswordFormResult;
581
656
 
657
+ /** `true` when `value` (after trimming) looks like an email address. */
658
+ declare function isValidForgotPasswordEmail(value: string): boolean;
659
+ interface UseForgotPasswordSubmitArgs {
660
+ /** The same-origin BFF client (build it with `createBffAuthClient`). */
661
+ client: BffAuthClient;
662
+ /**
663
+ * Full URL with a `{token}` placeholder, forwarded to the backend so it can
664
+ * build the reset-email link without hardcoding a frontend host.
665
+ */
666
+ resetUrlTemplate?: string;
667
+ /** Invoked once the request succeeds (the generic-confirmation state). */
668
+ onSuccess?: () => void;
669
+ }
670
+ interface UseForgotPasswordSubmitResult {
671
+ email: string;
672
+ setEmail: (value: string) => void;
673
+ /** `true` once the request has succeeded (show the generic confirmation). */
674
+ submitted: boolean;
675
+ /** `true` while the request is in flight. */
676
+ isSubmitting: boolean;
677
+ /** `true` when the last submit hit a network / 5xx failure. */
678
+ hasNetworkError: boolean;
679
+ /** `true` when the email is valid and no request is in flight. */
680
+ canSubmit: boolean;
681
+ /** Validate, then fire the request (no-op when `canSubmit` is false). */
682
+ submit: () => void;
683
+ /** Clear all state — call when the modal closes. */
684
+ reset: () => void;
685
+ }
686
+ /** Headless, react-query-free "request a reset link" logic. */
687
+ declare function useForgotPasswordSubmit({ client, resetUrlTemplate, onSuccess, }: UseForgotPasswordSubmitArgs): UseForgotPasswordSubmitResult;
688
+
582
689
  /**
583
690
  * The two discrete steps of the email-OTP login flow.
584
691
  *
@@ -803,4 +910,4 @@ declare function validatePasswordPolicy(password: string): PasswordPolicyError[]
803
910
  /** `true` when the password satisfies every policy rule. */
804
911
  declare function isPasswordValid(password: string): boolean;
805
912
 
806
- export { AuthTestIds, type AuthTheme, type AuthThemeColors, AuthThemeProvider, type AuthThemeProviderProps, type AuthThemeRadii, type AuthThemeSpacing, type AuthThemeTypography, BffAuthStatus, type CreateBffAuthClientOptions, DEFAULT_FORGOT_PASSWORD_LABELS, DEFAULT_LOGIN_LABELS, DEFAULT_OTP_LABELS, DEFAULT_PIN_LABELS, DEFAULT_RESET_PASSWORD_LABELS, ForgotPasswordForm, type ForgotPasswordFormLabels, type ForgotPasswordFormProps, LoginForm, type LoginFormLabels, type LoginFormProps, OtpForm, type OtpFormLabels, type OtpFormProps, OtpLoginStep, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, PasswordPolicyError, PinForm, type PinFormLabels, type PinFormProps, ResetPasswordError, ResetPasswordForm, type ResetPasswordFormLabels, type ResetPasswordFormProps, type RoleRoute, type RoleRouteTable, type UseBffAuthOptions, type UseBffAuthResult, type UseBffForgotPasswordOptions, type UseBffResetPasswordOptions, type UseOtpLoginOptions, type UseOtpLoginResult, type UsePinLoginOptions, type UsePinLoginResult, type UseResetPasswordFormArgs, type UseResetPasswordFormResult, collectUserRoles, createBffAuthClient, defaultAuthTheme, isPasswordValid, resolvePostLoginRoute, useAuthTheme, useBffAuth, useBffForgotPassword, useBffResetPassword, useOtpLogin, usePinLogin, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };
913
+ export { AuthTestIds, type AuthTheme, type AuthThemeColors, AuthThemeProvider, type AuthThemeProviderProps, type AuthThemeRadii, type AuthThemeSpacing, type AuthThemeTypography, BffAuthStatus, type CreateBffAuthClientOptions, DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS, DEFAULT_FORGOT_PASSWORD_LABELS, DEFAULT_LOGIN_LABELS, DEFAULT_OTP_LABELS, DEFAULT_PIN_LABELS, DEFAULT_RESET_PASSWORD_LABELS, ForgotPasswordFields, type ForgotPasswordFieldsLabels, type ForgotPasswordFieldsProps, ForgotPasswordForm, type ForgotPasswordFormLabels, type ForgotPasswordFormProps, LoginForm, type LoginFormLabels, type LoginFormProps, OtpForm, type OtpFormLabels, type OtpFormProps, OtpLoginStep, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, PasswordPolicyError, PinForm, type PinFormLabels, type PinFormProps, ResetPasswordError, ResetPasswordForm, type ResetPasswordFormLabels, type ResetPasswordFormProps, type RoleRoute, type RoleRouteTable, type UseBffAuthOptions, type UseBffAuthResult, type UseBffForgotPasswordOptions, type UseBffResetPasswordOptions, type UseForgotPasswordSubmitArgs, type UseForgotPasswordSubmitResult, type UseOtpLoginOptions, type UseOtpLoginResult, type UsePinLoginOptions, type UsePinLoginResult, type UseResetPasswordFormArgs, type UseResetPasswordFormResult, collectUserRoles, createBffAuthClient, defaultAuthTheme, isPasswordValid, isValidForgotPasswordEmail, resolvePostLoginRoute, useAuthTheme, useBffAuth, useBffForgotPassword, useBffResetPassword, useForgotPasswordSubmit, useOtpLogin, usePinLogin, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };
package/dist/index.d.ts CHANGED
@@ -28,6 +28,9 @@ interface LoginFormLabels {
28
28
  submitting: string;
29
29
  /** The "Forgot password?" link text. */
30
30
  forgotPassword: string;
31
+ /** The "Create account" link shown below the submit button when an
32
+ * `onSignUp` handler is supplied. Omit the handler to hide the link. */
33
+ signUp: string;
31
34
  /** Generic credentials-rejected message. */
32
35
  invalidCredentials: string;
33
36
  /** Shown when one or both fields are empty on submit. */
@@ -49,6 +52,17 @@ interface ForgotPasswordFormLabels {
49
52
  /** Shown when the entered value is not a valid email. */
50
53
  invalidEmail: string;
51
54
  }
55
+ /**
56
+ * Strings rendered by `<ForgotPasswordFields>` — the embedded modal body. It is
57
+ * the forgot-password form plus the two modal affordances (`cancel` alongside
58
+ * submit, `close` on the success state) that a screen-shaped form has no need of.
59
+ */
60
+ interface ForgotPasswordFieldsLabels extends ForgotPasswordFormLabels {
61
+ /** The "cancel" button shown alongside submit. */
62
+ cancel: string;
63
+ /** The "close" button shown on the success state. */
64
+ close: string;
65
+ }
52
66
  /** Strings rendered by `<OtpForm>` — the two-step email-OTP login form. */
53
67
  interface OtpFormLabels {
54
68
  /** Title shown on the request-a-code step. */
@@ -124,6 +138,7 @@ interface ResetPasswordFormLabels {
124
138
  }
125
139
  declare const DEFAULT_LOGIN_LABELS: LoginFormLabels;
126
140
  declare const DEFAULT_FORGOT_PASSWORD_LABELS: ForgotPasswordFormLabels;
141
+ declare const DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS: ForgotPasswordFieldsLabels;
127
142
  declare const DEFAULT_OTP_LABELS: OtpFormLabels;
128
143
  declare const DEFAULT_PIN_LABELS: PinFormLabels;
129
144
  declare const DEFAULT_RESET_PASSWORD_LABELS: ResetPasswordFormLabels;
@@ -233,11 +248,18 @@ interface LoginFormProps {
233
248
  onSuccess: (user: BffUser) => void;
234
249
  /** Called when the user taps the "Forgot password?" link. Omit to hide the link. */
235
250
  onForgotPassword?: () => void;
251
+ /**
252
+ * Called when the user taps the "Create account" link. Omit to hide the link
253
+ * (the registration UI then has to be reached some other way — direct nav,
254
+ * marketing CTA, etc.). The consuming app decides where the link routes;
255
+ * `<LoginForm>` just surfaces the entry point.
256
+ */
257
+ onSignUp?: () => void;
236
258
  /** Prefix applied to every `testID` so multiple forms can share a screen. */
237
259
  testIdPrefix?: string;
238
260
  }
239
261
  /** Themeable password-login form built on `useBffAuth`. */
240
- declare function LoginForm({ client, theme: themeProp, labels: labelsProp, onSuccess, onForgotPassword, testIdPrefix, }: Readonly<LoginFormProps>): ReactElement;
262
+ declare function LoginForm({ client, theme: themeProp, labels: labelsProp, onSuccess, onForgotPassword, onSignUp, testIdPrefix, }: Readonly<LoginFormProps>): ReactElement;
241
263
 
242
264
  /**
243
265
  * `<ForgotPasswordForm>` — the ready-made, themeable "request a reset link"
@@ -274,6 +296,56 @@ interface ForgotPasswordFormProps {
274
296
  /** Themeable "request a reset link" form built on `useBffForgotPassword`. */
275
297
  declare function ForgotPasswordForm({ client, theme: themeProp, labels: labelsProp, resetUrlTemplate, onSuccess, testIdPrefix, }: Readonly<ForgotPasswordFormProps>): ReactElement;
276
298
 
299
+ /**
300
+ * `<ForgotPasswordFields>` — the embedded, themeable "request a reset link"
301
+ * body, sized to live INSIDE an app-owned modal shell (not a full screen).
302
+ *
303
+ * The product apps each show "Forgot password?" as a modal launched from their
304
+ * login route, wrapping their own modal chrome (title bar + close affordance)
305
+ * around an identical email field + submit/cancel body. This is that body,
306
+ * promoted into the package so the three apps stop hand-rolling it.
307
+ *
308
+ * Built on the react-query-free `useForgotPasswordSubmit` hook, so it renders +
309
+ * submits on a provider-less login route (where `useMutation` would crash). The
310
+ * backend is anti-enumeration, so a successful request swaps to a generic
311
+ * confirmation that never reveals whether the address is registered.
312
+ *
313
+ * Differs from the screen-shaped `<ForgotPasswordForm>`: no outer screen/card
314
+ * wrapper and no title (the host modal owns those), plus two modal affordances —
315
+ * a `Cancel` button beside submit and a `Close` button on the success state —
316
+ * each rendered only when its handler is supplied.
317
+ */
318
+
319
+ interface ForgotPasswordFieldsProps {
320
+ /** The same-origin BFF client (build it with `createBffAuthClient`). */
321
+ client: BffAuthClient;
322
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
323
+ theme?: AuthTheme;
324
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
325
+ labels?: Partial<ForgotPasswordFieldsLabels>;
326
+ /**
327
+ * Full URL with a `{token}` placeholder, forwarded to the backend so it can
328
+ * build the reset-email link without hardcoding a frontend host.
329
+ */
330
+ resetUrlTemplate?: string;
331
+ /**
332
+ * The host modal's open state. When it flips to `false` the body clears
333
+ * itself, so reopening shows a fresh form regardless of which affordance
334
+ * closed it. Defaults to `true` for non-modal embedding.
335
+ */
336
+ visible?: boolean;
337
+ /** Called once the request succeeds (the generic-confirmation state). */
338
+ onSuccess?: () => void;
339
+ /** When supplied, renders a Cancel button beside submit. */
340
+ onCancel?: () => void;
341
+ /** When supplied, renders a Close button on the success state. */
342
+ onClose?: () => void;
343
+ /** Prefix applied to every `testID`. */
344
+ testIdPrefix?: string;
345
+ }
346
+ /** Embedded "request a reset link" body for an app-owned modal. */
347
+ declare function ForgotPasswordFields({ client, theme: themeProp, labels: labelsProp, resetUrlTemplate, visible, onSuccess, onCancel, onClose, testIdPrefix, }: Readonly<ForgotPasswordFieldsProps>): ReactElement;
348
+
277
349
  /**
278
350
  * `<ResetPasswordForm>` — the ready-made, themeable "choose a new password"
279
351
  * form.
@@ -423,10 +495,13 @@ declare const AuthTestIds: {
423
495
  readonly loginPasswordInput: "auth-login-password";
424
496
  readonly loginSubmitButton: "auth-login-submit";
425
497
  readonly loginForgotLink: "auth-login-forgot-link";
498
+ readonly loginSignUpLink: "auth-login-signup-link";
426
499
  readonly loginError: "auth-login-error";
427
500
  readonly forgotPasswordForm: "auth-forgot-form";
428
501
  readonly forgotPasswordEmailInput: "auth-forgot-email";
429
502
  readonly forgotPasswordSubmitButton: "auth-forgot-submit";
503
+ readonly forgotPasswordCancelButton: "auth-forgot-cancel";
504
+ readonly forgotPasswordCloseButton: "auth-forgot-close";
430
505
  readonly forgotPasswordError: "auth-forgot-error";
431
506
  readonly forgotPasswordSuccess: "auth-forgot-success";
432
507
  readonly resetPasswordForm: "auth-reset-form";
@@ -579,6 +654,38 @@ interface UseResetPasswordFormResult {
579
654
  */
580
655
  declare function useResetPasswordForm({ client, token, onSuccess, }: UseResetPasswordFormArgs): UseResetPasswordFormResult;
581
656
 
657
+ /** `true` when `value` (after trimming) looks like an email address. */
658
+ declare function isValidForgotPasswordEmail(value: string): boolean;
659
+ interface UseForgotPasswordSubmitArgs {
660
+ /** The same-origin BFF client (build it with `createBffAuthClient`). */
661
+ client: BffAuthClient;
662
+ /**
663
+ * Full URL with a `{token}` placeholder, forwarded to the backend so it can
664
+ * build the reset-email link without hardcoding a frontend host.
665
+ */
666
+ resetUrlTemplate?: string;
667
+ /** Invoked once the request succeeds (the generic-confirmation state). */
668
+ onSuccess?: () => void;
669
+ }
670
+ interface UseForgotPasswordSubmitResult {
671
+ email: string;
672
+ setEmail: (value: string) => void;
673
+ /** `true` once the request has succeeded (show the generic confirmation). */
674
+ submitted: boolean;
675
+ /** `true` while the request is in flight. */
676
+ isSubmitting: boolean;
677
+ /** `true` when the last submit hit a network / 5xx failure. */
678
+ hasNetworkError: boolean;
679
+ /** `true` when the email is valid and no request is in flight. */
680
+ canSubmit: boolean;
681
+ /** Validate, then fire the request (no-op when `canSubmit` is false). */
682
+ submit: () => void;
683
+ /** Clear all state — call when the modal closes. */
684
+ reset: () => void;
685
+ }
686
+ /** Headless, react-query-free "request a reset link" logic. */
687
+ declare function useForgotPasswordSubmit({ client, resetUrlTemplate, onSuccess, }: UseForgotPasswordSubmitArgs): UseForgotPasswordSubmitResult;
688
+
582
689
  /**
583
690
  * The two discrete steps of the email-OTP login flow.
584
691
  *
@@ -803,4 +910,4 @@ declare function validatePasswordPolicy(password: string): PasswordPolicyError[]
803
910
  /** `true` when the password satisfies every policy rule. */
804
911
  declare function isPasswordValid(password: string): boolean;
805
912
 
806
- export { AuthTestIds, type AuthTheme, type AuthThemeColors, AuthThemeProvider, type AuthThemeProviderProps, type AuthThemeRadii, type AuthThemeSpacing, type AuthThemeTypography, BffAuthStatus, type CreateBffAuthClientOptions, DEFAULT_FORGOT_PASSWORD_LABELS, DEFAULT_LOGIN_LABELS, DEFAULT_OTP_LABELS, DEFAULT_PIN_LABELS, DEFAULT_RESET_PASSWORD_LABELS, ForgotPasswordForm, type ForgotPasswordFormLabels, type ForgotPasswordFormProps, LoginForm, type LoginFormLabels, type LoginFormProps, OtpForm, type OtpFormLabels, type OtpFormProps, OtpLoginStep, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, PasswordPolicyError, PinForm, type PinFormLabels, type PinFormProps, ResetPasswordError, ResetPasswordForm, type ResetPasswordFormLabels, type ResetPasswordFormProps, type RoleRoute, type RoleRouteTable, type UseBffAuthOptions, type UseBffAuthResult, type UseBffForgotPasswordOptions, type UseBffResetPasswordOptions, type UseOtpLoginOptions, type UseOtpLoginResult, type UsePinLoginOptions, type UsePinLoginResult, type UseResetPasswordFormArgs, type UseResetPasswordFormResult, collectUserRoles, createBffAuthClient, defaultAuthTheme, isPasswordValid, resolvePostLoginRoute, useAuthTheme, useBffAuth, useBffForgotPassword, useBffResetPassword, useOtpLogin, usePinLogin, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };
913
+ export { AuthTestIds, type AuthTheme, type AuthThemeColors, AuthThemeProvider, type AuthThemeProviderProps, type AuthThemeRadii, type AuthThemeSpacing, type AuthThemeTypography, BffAuthStatus, type CreateBffAuthClientOptions, DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS, DEFAULT_FORGOT_PASSWORD_LABELS, DEFAULT_LOGIN_LABELS, DEFAULT_OTP_LABELS, DEFAULT_PIN_LABELS, DEFAULT_RESET_PASSWORD_LABELS, ForgotPasswordFields, type ForgotPasswordFieldsLabels, type ForgotPasswordFieldsProps, ForgotPasswordForm, type ForgotPasswordFormLabels, type ForgotPasswordFormProps, LoginForm, type LoginFormLabels, type LoginFormProps, OtpForm, type OtpFormLabels, type OtpFormProps, OtpLoginStep, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, PasswordPolicyError, PinForm, type PinFormLabels, type PinFormProps, ResetPasswordError, ResetPasswordForm, type ResetPasswordFormLabels, type ResetPasswordFormProps, type RoleRoute, type RoleRouteTable, type UseBffAuthOptions, type UseBffAuthResult, type UseBffForgotPasswordOptions, type UseBffResetPasswordOptions, type UseForgotPasswordSubmitArgs, type UseForgotPasswordSubmitResult, type UseOtpLoginOptions, type UseOtpLoginResult, type UsePinLoginOptions, type UsePinLoginResult, type UseResetPasswordFormArgs, type UseResetPasswordFormResult, collectUserRoles, createBffAuthClient, defaultAuthTheme, isPasswordValid, isValidForgotPasswordEmail, resolvePostLoginRoute, useAuthTheme, useBffAuth, useBffForgotPassword, useBffResetPassword, useForgotPasswordSubmit, useOtpLogin, usePinLogin, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ var DEFAULT_LOGIN_LABELS = {
19
19
  submit: "Sign in",
20
20
  submitting: "Signing in...",
21
21
  forgotPassword: "Forgot password?",
22
+ signUp: "Create an account",
22
23
  invalidCredentials: "Incorrect username or password.",
23
24
  missingFields: "Enter both your username and password."
24
25
  };
@@ -33,6 +34,11 @@ var DEFAULT_FORGOT_PASSWORD_LABELS = {
33
34
  networkError: "Something went wrong. Please try again.",
34
35
  invalidEmail: "Enter a valid email address."
35
36
  };
37
+ var DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS = {
38
+ ...DEFAULT_FORGOT_PASSWORD_LABELS,
39
+ cancel: "Cancel",
40
+ close: "Close"
41
+ };
36
42
  var DEFAULT_OTP_LABELS = {
37
43
  requestTitle: "Sign in with a code",
38
44
  requestDescription: "Enter your email and we will send you a one-time code.",
@@ -86,10 +92,13 @@ var AuthTestIds = {
86
92
  loginPasswordInput: "auth-login-password",
87
93
  loginSubmitButton: "auth-login-submit",
88
94
  loginForgotLink: "auth-login-forgot-link",
95
+ loginSignUpLink: "auth-login-signup-link",
89
96
  loginError: "auth-login-error",
90
97
  forgotPasswordForm: "auth-forgot-form",
91
98
  forgotPasswordEmailInput: "auth-forgot-email",
92
99
  forgotPasswordSubmitButton: "auth-forgot-submit",
100
+ forgotPasswordCancelButton: "auth-forgot-cancel",
101
+ forgotPasswordCloseButton: "auth-forgot-close",
93
102
  forgotPasswordError: "auth-forgot-error",
94
103
  forgotPasswordSuccess: "auth-forgot-success",
95
104
  resetPasswordForm: "auth-reset-form",
@@ -359,6 +368,7 @@ function LoginForm({
359
368
  labels: labelsProp,
360
369
  onSuccess,
361
370
  onForgotPassword,
371
+ onSignUp,
362
372
  testIdPrefix
363
373
  }) {
364
374
  const theme = useAuthTheme(themeProp);
@@ -465,7 +475,20 @@ function LoginForm({
465
475
  onPress: handleSubmit,
466
476
  children: isSubmitting ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.onPrimary, size: "small" }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.primaryButtonText, children: labels.submit })
467
477
  }
468
- )
478
+ ),
479
+ onSignUp !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
480
+ reactNative.TouchableOpacity,
481
+ {
482
+ accessibilityHint: labels.signUp,
483
+ accessibilityLabel: labels.signUp,
484
+ accessibilityRole: "link",
485
+ disabled: isSubmitting,
486
+ style: styles.fieldGroup,
487
+ testID: withTestIdPrefix(AuthTestIds.loginSignUpLink, testIdPrefix),
488
+ onPress: onSignUp,
489
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.linkText, children: labels.signUp })
490
+ }
491
+ ) : null
469
492
  ] }) });
470
493
  }
471
494
  function useBffForgotPassword(options) {
@@ -596,6 +619,182 @@ function ForgotPasswordForm({
596
619
  }
597
620
  ) });
598
621
  }
622
+ var EMAIL_REGEX2 = /^[^@\s]+@[^.\s]+\.[^\s]+$/;
623
+ function isValidForgotPasswordEmail(value) {
624
+ return EMAIL_REGEX2.test(value.trim());
625
+ }
626
+ function useForgotPasswordSubmit({
627
+ client,
628
+ resetUrlTemplate,
629
+ onSuccess
630
+ }) {
631
+ const [email, setEmail] = react.useState("");
632
+ const [submitted, setSubmitted] = react.useState(false);
633
+ const [isSubmitting, setIsSubmitting] = react.useState(false);
634
+ const [hasNetworkError, setHasNetworkError] = react.useState(false);
635
+ const canSubmit = isValidForgotPasswordEmail(email) && !isSubmitting;
636
+ const submit = react.useCallback(() => {
637
+ const target = email.trim();
638
+ if (!isValidForgotPasswordEmail(target) || isSubmitting) {
639
+ return;
640
+ }
641
+ setHasNetworkError(false);
642
+ setIsSubmitting(true);
643
+ const request = { email: target, resetUrlTemplate };
644
+ client.forgotPassword(request).then(() => {
645
+ setSubmitted(true);
646
+ onSuccess?.();
647
+ }).catch(() => setHasNetworkError(true)).finally(() => setIsSubmitting(false));
648
+ }, [client, email, isSubmitting, resetUrlTemplate, onSuccess]);
649
+ const reset = react.useCallback(() => {
650
+ setEmail("");
651
+ setSubmitted(false);
652
+ setIsSubmitting(false);
653
+ setHasNetworkError(false);
654
+ }, []);
655
+ return {
656
+ email,
657
+ setEmail,
658
+ submitted,
659
+ isSubmitting,
660
+ hasNetworkError,
661
+ canSubmit,
662
+ submit,
663
+ reset
664
+ };
665
+ }
666
+ var SECONDARY_BORDER_WIDTH = 1;
667
+ function useModalStyles(theme) {
668
+ return react.useMemo(
669
+ () => reactNative.StyleSheet.create({
670
+ body: { padding: theme.spacing.md },
671
+ actions: {
672
+ flexDirection: "row",
673
+ justifyContent: "flex-end",
674
+ alignItems: "center",
675
+ marginTop: theme.spacing.md,
676
+ gap: theme.spacing.sm
677
+ },
678
+ secondaryButton: {
679
+ borderRadius: theme.radii.input,
680
+ paddingVertical: theme.spacing.sm,
681
+ paddingHorizontal: theme.spacing.md,
682
+ borderWidth: SECONDARY_BORDER_WIDTH,
683
+ borderColor: theme.colors.border
684
+ },
685
+ secondaryButtonText: {
686
+ fontSize: theme.typography.body,
687
+ fontWeight: "600",
688
+ color: theme.colors.text
689
+ }
690
+ }),
691
+ [theme]
692
+ );
693
+ }
694
+ function ForgotPasswordFields({
695
+ client,
696
+ theme: themeProp,
697
+ labels: labelsProp,
698
+ resetUrlTemplate,
699
+ visible = true,
700
+ onSuccess,
701
+ onCancel,
702
+ onClose,
703
+ testIdPrefix
704
+ }) {
705
+ const theme = useAuthTheme(themeProp);
706
+ const styles = useAuthStyles(theme);
707
+ const modalStyles = useModalStyles(theme);
708
+ const labels = react.useMemo(
709
+ () => ({ ...DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS, ...labelsProp }),
710
+ [labelsProp]
711
+ );
712
+ const form = useForgotPasswordSubmit({ client, resetUrlTemplate, onSuccess });
713
+ const { reset } = form;
714
+ react.useEffect(() => {
715
+ if (!visible) {
716
+ reset();
717
+ }
718
+ }, [visible, reset]);
719
+ const submitButtonStyle = form.canSubmit ? styles.primaryButton : [styles.primaryButton, styles.primaryButtonDisabled];
720
+ const handleCancel = () => {
721
+ form.reset();
722
+ onCancel?.();
723
+ };
724
+ const handleClose = () => {
725
+ form.reset();
726
+ onClose?.();
727
+ };
728
+ if (form.submitted) {
729
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: modalStyles.body, testID: withTestIdPrefix(AuthTestIds.forgotPasswordForm, testIdPrefix), children: [
730
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.successText, testID: withTestIdPrefix(AuthTestIds.forgotPasswordSuccess, testIdPrefix), children: labels.successMessage }),
731
+ onClose !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: modalStyles.actions, children: /* @__PURE__ */ jsxRuntime.jsx(
732
+ reactNative.TouchableOpacity,
733
+ {
734
+ accessibilityHint: labels.close,
735
+ accessibilityLabel: labels.close,
736
+ accessibilityRole: "button",
737
+ style: styles.primaryButton,
738
+ testID: withTestIdPrefix(AuthTestIds.forgotPasswordCloseButton, testIdPrefix),
739
+ onPress: handleClose,
740
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.primaryButtonText, children: labels.close })
741
+ }
742
+ ) }) : null
743
+ ] });
744
+ }
745
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: modalStyles.body, testID: withTestIdPrefix(AuthTestIds.forgotPasswordForm, testIdPrefix), children: [
746
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.subtitle, children: labels.description }),
747
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.fieldGroup, children: [
748
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.label, children: labels.emailLabel }),
749
+ /* @__PURE__ */ jsxRuntime.jsx(
750
+ reactNative.TextInput,
751
+ {
752
+ accessibilityHint: labels.emailPlaceholder,
753
+ accessibilityLabel: labels.emailLabel,
754
+ autoCapitalize: "none",
755
+ autoCorrect: false,
756
+ editable: !form.isSubmitting,
757
+ keyboardType: "email-address",
758
+ placeholder: labels.emailPlaceholder,
759
+ placeholderTextColor: theme.colors.textSecondary,
760
+ style: styles.input,
761
+ testID: withTestIdPrefix(AuthTestIds.forgotPasswordEmailInput, testIdPrefix),
762
+ value: form.email,
763
+ onChangeText: form.setEmail
764
+ }
765
+ )
766
+ ] }),
767
+ form.hasNetworkError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.errorText, testID: withTestIdPrefix(AuthTestIds.forgotPasswordError, testIdPrefix), children: labels.networkError }) : null,
768
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: modalStyles.actions, children: [
769
+ onCancel !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
770
+ reactNative.TouchableOpacity,
771
+ {
772
+ accessibilityHint: labels.cancel,
773
+ accessibilityLabel: labels.cancel,
774
+ accessibilityRole: "button",
775
+ disabled: form.isSubmitting,
776
+ style: modalStyles.secondaryButton,
777
+ testID: withTestIdPrefix(AuthTestIds.forgotPasswordCancelButton, testIdPrefix),
778
+ onPress: handleCancel,
779
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: modalStyles.secondaryButtonText, children: labels.cancel })
780
+ }
781
+ ) : null,
782
+ /* @__PURE__ */ jsxRuntime.jsx(
783
+ reactNative.TouchableOpacity,
784
+ {
785
+ accessibilityHint: labels.submit,
786
+ accessibilityLabel: form.isSubmitting ? labels.submitting : labels.submit,
787
+ accessibilityRole: "button",
788
+ disabled: !form.canSubmit,
789
+ style: submitButtonStyle,
790
+ testID: withTestIdPrefix(AuthTestIds.forgotPasswordSubmitButton, testIdPrefix),
791
+ onPress: form.submit,
792
+ children: form.isSubmitting ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.onPrimary, size: "small" }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.primaryButtonText, children: labels.submit })
793
+ }
794
+ )
795
+ ] })
796
+ ] });
797
+ }
599
798
 
600
799
  // src/hooks/ResetPasswordError.ts
601
800
  var ResetPasswordError = /* @__PURE__ */ ((ResetPasswordError2) => {
@@ -1097,9 +1296,9 @@ function useOtpLogin(options) {
1097
1296
  ]
1098
1297
  );
1099
1298
  }
1100
- var EMAIL_REGEX2 = /^[^@\s]+@[^.\s]+\.[^\s]+$/;
1299
+ var EMAIL_REGEX3 = /^[^@\s]+@[^.\s]+\.[^\s]+$/;
1101
1300
  function isValidEmail2(value) {
1102
- return EMAIL_REGEX2.test(value);
1301
+ return EMAIL_REGEX3.test(value);
1103
1302
  }
1104
1303
  function transportErrorFor(step, error, labels) {
1105
1304
  if (error === null) {
@@ -1380,11 +1579,13 @@ Object.defineProperty(exports, "createFetchHttpClient", {
1380
1579
  exports.AuthTestIds = AuthTestIds;
1381
1580
  exports.AuthThemeProvider = AuthThemeProvider;
1382
1581
  exports.BffAuthStatus = BffAuthStatus;
1582
+ exports.DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS = DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS;
1383
1583
  exports.DEFAULT_FORGOT_PASSWORD_LABELS = DEFAULT_FORGOT_PASSWORD_LABELS;
1384
1584
  exports.DEFAULT_LOGIN_LABELS = DEFAULT_LOGIN_LABELS;
1385
1585
  exports.DEFAULT_OTP_LABELS = DEFAULT_OTP_LABELS;
1386
1586
  exports.DEFAULT_PIN_LABELS = DEFAULT_PIN_LABELS;
1387
1587
  exports.DEFAULT_RESET_PASSWORD_LABELS = DEFAULT_RESET_PASSWORD_LABELS;
1588
+ exports.ForgotPasswordFields = ForgotPasswordFields;
1388
1589
  exports.ForgotPasswordForm = ForgotPasswordForm;
1389
1590
  exports.LoginForm = LoginForm;
1390
1591
  exports.OtpForm = OtpForm;
@@ -1399,11 +1600,13 @@ exports.collectUserRoles = collectUserRoles;
1399
1600
  exports.createBffAuthClient = createBffAuthClient;
1400
1601
  exports.defaultAuthTheme = defaultAuthTheme;
1401
1602
  exports.isPasswordValid = isPasswordValid;
1603
+ exports.isValidForgotPasswordEmail = isValidForgotPasswordEmail;
1402
1604
  exports.resolvePostLoginRoute = resolvePostLoginRoute;
1403
1605
  exports.useAuthTheme = useAuthTheme;
1404
1606
  exports.useBffAuth = useBffAuth;
1405
1607
  exports.useBffForgotPassword = useBffForgotPassword;
1406
1608
  exports.useBffResetPassword = useBffResetPassword;
1609
+ exports.useForgotPasswordSubmit = useForgotPasswordSubmit;
1407
1610
  exports.useOtpLogin = useOtpLogin;
1408
1611
  exports.usePinLogin = usePinLogin;
1409
1612
  exports.useResetPasswordForm = useResetPasswordForm;