@dloizides/auth-web 1.3.0 → 1.5.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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactElement, ReactNode } from 'react';
2
- import { BffAuthClient, BffUser, BffLoginRequest, BffForgotPasswordRequest, BffResetPasswordRequest, BffOtpRequestResult, HttpClient } from '@dloizides/auth-client';
2
+ import { BffAuthClient, BffUser, HttpClient, BffLoginRequest, BffForgotPasswordRequest, BffResetPasswordRequest, BffOtpRequestResult } from '@dloizides/auth-client';
3
3
  export { BffAuthClient, BffAuthClientOptions, BffForgotPasswordRequest, BffLoginRequest, BffOtpRequestRequest, BffOtpRequestResult, BffOtpVerifyRequest, BffPinLoginRequest, BffRegisterRequest, BffResetPasswordRequest, BffUser, HttpClient, HttpRequest, HttpResponse, createFetchHttpClient } from '@dloizides/auth-client';
4
4
  import { UseMutationOptions, UseMutationResult } from '@tanstack/react-query';
5
5
 
@@ -115,6 +115,149 @@ interface PinFormLabels {
115
115
  /** Generic "that PIN was wrong / expired / locked out" message. */
116
116
  invalidPin: string;
117
117
  }
118
+ /**
119
+ * Strings rendered by `<DevicePinUnlockScreen>` — the returning-device unlock
120
+ * gate. `{name}` / `{count}` placeholders are substituted by the component.
121
+ */
122
+ interface DevicePinUnlockLabels {
123
+ /** Greeting with the remembered username; `{name}` is substituted. */
124
+ title: string;
125
+ /** Greeting when no username is remembered. */
126
+ titleNoName: string;
127
+ /** Copy above the PIN pad; `{count}` is substituted with the PIN length. */
128
+ description: string;
129
+ pinLabel: string;
130
+ pinPlaceholder: string;
131
+ /** The "unlock" submit button. */
132
+ submit: string;
133
+ /** Shown while the unlock is in flight. */
134
+ submitting: string;
135
+ /** The "sign in with password instead" escape link. */
136
+ usePasswordInstead: string;
137
+ /** Accessibility hint for the escape link. */
138
+ usePasswordHint: string;
139
+ /** Error: not enough digits entered; `{count}` is substituted. */
140
+ errorIncomplete: string;
141
+ /** Error: wrong PIN / unknown device (generic). */
142
+ errorInvalid: string;
143
+ /** Error: locked out, no retry hint. */
144
+ errorLockedOut: string;
145
+ /** Error: locked out with a retry hint; `{count}` is the seconds. */
146
+ errorLockedOutRetry: string;
147
+ /** Error: rate-limited, no retry hint. */
148
+ errorRateLimited: string;
149
+ /** Error: rate-limited with a retry hint; `{count}` is the seconds. */
150
+ errorRateLimitedRetry: string;
151
+ /** Error: anything else (network / unexpected). */
152
+ errorGeneric: string;
153
+ /** Accessibility label for a filled PIN dot. */
154
+ digitFilledHint: string;
155
+ /** Accessibility label for an empty PIN dot. */
156
+ digitEmptyHint: string;
157
+ }
158
+ /**
159
+ * Strings rendered by `<DevicePinEnrollForm>` (and the length picker it hosts)
160
+ * plus the `<DevicePinOffer>` wrapper. `{count}` placeholders are substituted.
161
+ */
162
+ interface DevicePinEnrollLabels {
163
+ offerTitle: string;
164
+ offerDescription: string;
165
+ offerAccept: string;
166
+ offerAcceptHint: string;
167
+ offerSkip: string;
168
+ offerSkipHint: string;
169
+ formTitle: string;
170
+ /** Copy above the form; `{count}` is substituted with the chosen length. */
171
+ formDescription: string;
172
+ lengthLabel: string;
173
+ /** Accessibility hint for a length pill; `{count}` is substituted. */
174
+ lengthOptionHint: string;
175
+ pinLabel: string;
176
+ pinPlaceholder: string;
177
+ confirmLabel: string;
178
+ confirmPlaceholder: string;
179
+ submit: string;
180
+ submitting: string;
181
+ cancel: string;
182
+ cancelHint: string;
183
+ /** Error: PINs do not match / wrong length; `{count}` is substituted. */
184
+ errorMismatch: string;
185
+ /** Error: the current session is not authenticated. */
186
+ errorUnauthorized: string;
187
+ /** Error: the session already has a PIN / is forbidden. */
188
+ errorForbidden: string;
189
+ /** Error: the PIN failed server-side validation. */
190
+ errorInvalidPin: string;
191
+ /** Error: any other failure. */
192
+ errorFailed: string;
193
+ }
194
+ /** Strings rendered by `<DevicePinSettingsCard>` — the account toggle. */
195
+ interface DevicePinSettingsLabels {
196
+ title: string;
197
+ description: string;
198
+ /** Status line when a device PIN is enabled. */
199
+ statusEnabled: string;
200
+ /** Status line when no device PIN is set. */
201
+ statusDisabled: string;
202
+ /** The "enable" button. */
203
+ enable: string;
204
+ enableHint: string;
205
+ /** The "disable" button. */
206
+ disable: string;
207
+ disableHint: string;
208
+ /** Shown while disabling is in flight. */
209
+ disabling: string;
210
+ /** Error shown when disabling fails. */
211
+ disableFailed: string;
212
+ }
213
+ /** Strings rendered by `<PasskeyLoginButton>` — the login-surface passkey CTA. */
214
+ interface PasskeyLoginLabels {
215
+ /** The "sign in with a passkey" button. */
216
+ signInButton: string;
217
+ signInHint: string;
218
+ /** Error banner when the ceremony was cancelled. */
219
+ errorCancelled: string;
220
+ /** Error banner when the ceremony failed. */
221
+ errorFailed: string;
222
+ }
223
+ /** Strings rendered by `<PasskeySettingsCard>` — the account passkey card. */
224
+ interface PasskeySettingsLabels {
225
+ title: string;
226
+ description: string;
227
+ /** Italic hint about the re-auth step. */
228
+ reauthHint: string;
229
+ /** The "add a passkey" button. */
230
+ addButton: string;
231
+ addHint: string;
232
+ /** Success line shown after returning from a registration. */
233
+ registeredSuccess: string;
234
+ }
235
+ /** Strings rendered by `<PreferredMethodSettingsCard>` — the cross-device default-method picker. */
236
+ interface PreferredMethodSettingsLabels {
237
+ title: string;
238
+ description: string;
239
+ /** The "no preference / ask me each time" option. */
240
+ noPreference: string;
241
+ /** Display name for the password method option. */
242
+ methodPassword: string;
243
+ /** Display name for the device-PIN method option. */
244
+ methodPin: string;
245
+ /** Display name for the passkey method option. */
246
+ methodPasskey: string;
247
+ /** Shown while a selection is being saved. */
248
+ saving: string;
249
+ /** Error shown when saving the preference fails. */
250
+ saveFailed: string;
251
+ }
252
+ /** Strings rendered by `<PreferredMethodHint>` — the login-screen "you usually sign in with…" line. */
253
+ interface PreferredMethodHintLabels {
254
+ /** Full sentence shown when the cached preference is password. */
255
+ password: string;
256
+ /** Full sentence shown when the cached preference is device-PIN. */
257
+ pin: string;
258
+ /** Full sentence shown when the cached preference is passkey. */
259
+ passkey: string;
260
+ }
118
261
  /** Strings rendered by `<ResetPasswordForm>`. */
119
262
  interface ResetPasswordFormLabels {
120
263
  title: string;
@@ -141,6 +284,13 @@ declare const DEFAULT_FORGOT_PASSWORD_LABELS: ForgotPasswordFormLabels;
141
284
  declare const DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS: ForgotPasswordFieldsLabels;
142
285
  declare const DEFAULT_OTP_LABELS: OtpFormLabels;
143
286
  declare const DEFAULT_PIN_LABELS: PinFormLabels;
287
+ declare const DEFAULT_DEVICE_PIN_UNLOCK_LABELS: DevicePinUnlockLabels;
288
+ declare const DEFAULT_DEVICE_PIN_ENROLL_LABELS: DevicePinEnrollLabels;
289
+ declare const DEFAULT_DEVICE_PIN_SETTINGS_LABELS: DevicePinSettingsLabels;
290
+ declare const DEFAULT_PASSKEY_LOGIN_LABELS: PasskeyLoginLabels;
291
+ declare const DEFAULT_PASSKEY_SETTINGS_LABELS: PasskeySettingsLabels;
292
+ declare const DEFAULT_PREFERRED_METHOD_SETTINGS_LABELS: PreferredMethodSettingsLabels;
293
+ declare const DEFAULT_PREFERRED_METHOD_HINT_LABELS: PreferredMethodHintLabels;
144
294
  declare const DEFAULT_RESET_PASSWORD_LABELS: ResetPasswordFormLabels;
145
295
 
146
296
  /**
@@ -447,6 +597,544 @@ interface PinFormProps {
447
597
  /** Themeable single-step event-PIN login form built on `usePinLogin`. */
448
598
  declare function PinForm({ client, eventExternalId, theme: themeProp, labels: labelsProp, onSuccess, testIdPrefix, }: Readonly<PinFormProps>): ReactElement;
449
599
 
600
+ /**
601
+ * A masked numeric PIN input for the device-PIN unlock + enrol surfaces.
602
+ *
603
+ * A single hidden secure `TextInput` captures the digits (numeric keyboard,
604
+ * length-capped, digits-only); a row of dot cells visualises how many have been
605
+ * entered. Tapping anywhere on the row focuses the field. Purely presentational
606
+ * — all submit / error logic lives in the unlock / enrol hooks. Styled from the
607
+ * `AuthTheme` token bag (no hardcoded brand colours).
608
+ */
609
+
610
+ interface DevicePinInputProps {
611
+ /** The current PIN value (digits only). */
612
+ value: string;
613
+ /** Number of dot cells to render — the enrolled / chosen PIN length. */
614
+ length: number;
615
+ /** Disable input while a request is in flight. */
616
+ disabled: boolean;
617
+ /** Base testID for the underlying field (a11y + E2E targeting). */
618
+ testID: string;
619
+ /** Optional prefix applied to the testID. */
620
+ testIdPrefix?: string;
621
+ /** Accessibility label for the field. */
622
+ accessibilityLabel: string;
623
+ /** Accessibility hint for the field. */
624
+ accessibilityHint: string;
625
+ /** Accessibility label for a filled dot. */
626
+ filledHint: string;
627
+ /** Accessibility label for an empty dot. */
628
+ emptyHint: string;
629
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
630
+ theme?: AuthTheme;
631
+ /** Emit the next digits-only value (already length-capped). */
632
+ onChange: (next: string) => void;
633
+ }
634
+ /** Themeable masked PIN pad — dots reflect entered digits; a hidden field captures them. */
635
+ declare function DevicePinInput({ value, length, disabled, testID, testIdPrefix, accessibilityLabel, accessibilityHint, filledHint, emptyHint, theme: themeProp, onChange, }: Readonly<DevicePinInputProps>): ReactElement;
636
+
637
+ /**
638
+ * The PIN-length (4/6/8) picker row for `<DevicePinEnrollForm>`.
639
+ *
640
+ * Presentational: renders one pill per allowed length and reports the chosen
641
+ * one. Styled from the `AuthTheme` token bag. Extracted from the enrol form to
642
+ * keep that component focused.
643
+ */
644
+
645
+ interface DevicePinLengthPickerProps {
646
+ /** The currently-selected PIN length. */
647
+ selected: number;
648
+ /** Disable while a request is in flight. */
649
+ disabled: boolean;
650
+ /** Accessibility hint template for a pill; `{count}` is substituted. */
651
+ optionHint: string;
652
+ /** Optional prefix applied to each pill's testID. */
653
+ testIdPrefix?: string;
654
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
655
+ theme?: AuthTheme;
656
+ /** Report the newly-chosen length. */
657
+ onSelect: (digits: number) => void;
658
+ }
659
+ /** Themeable 4/6/8 length chooser. */
660
+ declare function DevicePinLengthPicker({ selected, disabled, optionHint, testIdPrefix, theme: themeProp, onSelect, }: Readonly<DevicePinLengthPickerProps>): ReactElement;
661
+
662
+ /**
663
+ * The localizable error states the device-PIN UNLOCK surface can show.
664
+ *
665
+ * Kept deliberately coarse so the UI never reveals whether a failed unlock was a
666
+ * wrong PIN or an unknown / revoked device (both map to `Invalid`). `LockedOut`
667
+ * and `RateLimited` carry no count — the human-readable "try again later" copy
668
+ * is built from the `retryAfterSeconds` hint at the call site, not encoded here.
669
+ */
670
+ declare const enum DevicePinErrorKey {
671
+ /** The PIN entered was too short for the enrolled length. */
672
+ Incomplete = "incomplete",
673
+ /** Wrong PIN, or unknown / revoked device. Generic on purpose. */
674
+ Invalid = "invalid",
675
+ /** Locked out after too many wrong attempts. */
676
+ LockedOut = "locked_out",
677
+ /** Rate-limited by the BFF (too many requests). */
678
+ RateLimited = "rate_limited",
679
+ /** Anything else (network / unexpected). */
680
+ Generic = "generic"
681
+ }
682
+
683
+ /**
684
+ * Structural prop/result contracts for the device-PIN surfaces.
685
+ *
686
+ * `@dloizides/auth-web` deliberately does NOT take a hard dependency on the
687
+ * device-PIN methods that `@dloizides/auth-client` 3.3.0 adds. Instead the
688
+ * components depend on the **structural** shapes below — exactly what each one
689
+ * needs and no more. A real client (auth-client 3.3.0) satisfies
690
+ * {@link DevicePinCapableClient} by duck-typing; an app can equally pass a thin
691
+ * hand-rolled object. This keeps the two packages decoupled and lets a consumer
692
+ * upgrade either independently.
693
+ *
694
+ * The discriminated result shapes mirror the BFF's meaningfully-distinct
695
+ * outcomes (a wrong PIN, a lockout with a retry hint, a rate-limit, a generic
696
+ * error) so the unlock / enrol UIs can route on each one rather than collapsing
697
+ * them into a single opaque failure.
698
+ */
699
+ /**
700
+ * The per-device half of `GET /bff/config` — what this device remembers about
701
+ * the last user, all fields safe-defaulted by the parser when absent.
702
+ */
703
+ interface BffDeviceState {
704
+ /** Non-secret username this device remembers, or `null` when none. */
705
+ rememberedUsername: string | null;
706
+ /** `true` when this device has an enrolled device PIN. */
707
+ hasPin: boolean;
708
+ /** The enrolled PIN length (4/6/8), or `null` when unknown / no PIN. */
709
+ pinDigits: number | null;
710
+ /** The device-local preferred-method hint (e.g. `"pin"`), or `null`. */
711
+ preferredMethod: string | null;
712
+ }
713
+ /** The shape `GET /bff/config` resolves to (as the client parses it). */
714
+ interface BffLoginConfig {
715
+ /** The enabled login methods, lower-cased (e.g. `["password", "otp"]`). */
716
+ methods: string[];
717
+ /** `true` when self-serve registration is enabled for this BFF. */
718
+ registrationEnabled: boolean;
719
+ /** The per-device remembered state. */
720
+ deviceState: BffDeviceState;
721
+ }
722
+ /**
723
+ * Discriminated result of a device-PIN UNLOCK attempt. The client never rejects;
724
+ * it resolves one of these so the UI can route on the distinct outcomes (a
725
+ * lockout / rate-limit carries an optional `retryAfterSeconds` hint).
726
+ */
727
+ type DevicePinUnlockResult = {
728
+ status: 'success';
729
+ user: {
730
+ [claim: string]: unknown;
731
+ };
732
+ } | {
733
+ status: 'invalid';
734
+ } | {
735
+ status: 'locked';
736
+ retryAfterSeconds: number | null;
737
+ } | {
738
+ status: 'rateLimited';
739
+ retryAfterSeconds: number | null;
740
+ } | {
741
+ status: 'error';
742
+ };
743
+ /**
744
+ * Discriminated result of a device-PIN ENROL attempt. The client never rejects;
745
+ * it resolves one of these so the form can show the precise failure cause.
746
+ */
747
+ type DevicePinEnrollResult = {
748
+ status: 'success';
749
+ } | {
750
+ status: 'unauthorized';
751
+ } | {
752
+ status: 'forbidden';
753
+ } | {
754
+ status: 'invalidPin';
755
+ } | {
756
+ status: 'error';
757
+ };
758
+ /**
759
+ * The client capability the device-PIN components depend on. `auth-client`
760
+ * 3.3.0 satisfies this structurally; a consuming app may also pass a thin
761
+ * hand-rolled object exposing exactly these four methods.
762
+ */
763
+ interface DevicePinCapableClient {
764
+ /** Read `GET /bff/config` (methods + per-device state). */
765
+ getLoginConfig(): Promise<BffLoginConfig>;
766
+ /** Bind a device PIN of `digits` length to the current session. */
767
+ enrollDevicePin(request: {
768
+ pin: string;
769
+ digits: number;
770
+ }): Promise<DevicePinEnrollResult>;
771
+ /** Re-establish a session from a remembered device using `pin`. */
772
+ unlockWithDevicePin(request: {
773
+ pin: string;
774
+ }): Promise<DevicePinUnlockResult>;
775
+ /** Drop the device PIN for the current session. Resolves `true` on success. */
776
+ disableDevicePin(): Promise<boolean>;
777
+ }
778
+ /** The minimal slice {@link useBffLoginConfig} needs from a client. */
779
+ interface LoginConfigCapableClient {
780
+ getLoginConfig(): Promise<BffLoginConfig>;
781
+ }
782
+ /** The minimal slice {@link useDevicePinUnlock} needs from a client. */
783
+ interface DevicePinUnlockCapableClient {
784
+ unlockWithDevicePin(request: {
785
+ pin: string;
786
+ }): Promise<DevicePinUnlockResult>;
787
+ }
788
+ /** The minimal slice {@link useDevicePinEnroll} needs from a client. */
789
+ interface DevicePinEnrollCapableClient {
790
+ enrollDevicePin(request: {
791
+ pin: string;
792
+ digits: number;
793
+ }): Promise<DevicePinEnrollResult>;
794
+ }
795
+ /** The minimal slice {@link DevicePinSettingsCard} needs to disable a PIN. */
796
+ interface DevicePinDisableCapableClient {
797
+ disableDevicePin(): Promise<boolean>;
798
+ }
799
+
800
+ /** The signed-in user shape the success path hands back (claim bag). */
801
+ type DevicePinUnlockedUser = {
802
+ [claim: string]: unknown;
803
+ };
804
+ interface UseDevicePinUnlockArgs {
805
+ /** The client exposing `unlockWithDevicePin`. */
806
+ client: DevicePinUnlockCapableClient;
807
+ /** Enrolled PIN length (4/6/8) — a shorter entry is rejected before any call. */
808
+ digits: number;
809
+ /** Called with the fresh user after a successful unlock. */
810
+ onSignedIn: (user: DevicePinUnlockedUser) => void;
811
+ }
812
+ interface UseDevicePinUnlockResult {
813
+ /** The PIN currently entered. */
814
+ pin: string;
815
+ /** `true` while an unlock call is in flight. */
816
+ submitting: boolean;
817
+ /** The current error key, or `null` when there is none. */
818
+ errorKey: DevicePinErrorKey | null;
819
+ /** Seconds until retry, set with a `LockedOut`/`RateLimited` error; else `null`. */
820
+ retryAfterSeconds: number | null;
821
+ /** Replace the entered PIN. Clears a stale error. */
822
+ setPin: (next: string) => void;
823
+ /** Run the unlock attempt for the current PIN. */
824
+ submit: () => void;
825
+ }
826
+ declare function useDevicePinUnlock({ client, digits, onSignedIn, }: UseDevicePinUnlockArgs): UseDevicePinUnlockResult;
827
+
828
+ /**
829
+ * The device-PIN UNLOCK gate — shown on the login route for a returning,
830
+ * logged-OUT user whose device is remembered (`hasPin && rememberedUsername`).
831
+ *
832
+ * Renders a masked PIN pad of the enrolled length; on submit it calls
833
+ * `client.unlockWithDevicePin` (via {@link useDevicePinUnlock}) and, on success,
834
+ * routes through the SAME `onSignedIn` path as the password tab. A
835
+ * "Sign in with password instead" escape calls `onUsePassword` (the caller swaps
836
+ * this screen out). All five result statuses are handled, including the lockout /
837
+ * rate-limit retry countdown surfaced from `retryAfterSeconds`.
838
+ *
839
+ * react-query-FREE: all logic lives in `useDevicePinUnlock`, which uses plain
840
+ * `useState` + `.then()` over the client — never `useMutation`. Presentational
841
+ * only here. Distinct from the event-staff PIN (`PinForm`); this is `devicePin`.
842
+ */
843
+
844
+ interface DevicePinUnlockScreenProps {
845
+ /** The client exposing `unlockWithDevicePin`. */
846
+ client: DevicePinUnlockCapableClient;
847
+ /** Enrolled PIN length (4/6/8). */
848
+ digits: number;
849
+ /** Remembered username, shown in the greeting (may be empty). */
850
+ rememberedUsername: string;
851
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
852
+ theme?: AuthTheme;
853
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
854
+ labels?: Partial<DevicePinUnlockLabels>;
855
+ /** Prefix applied to every `testID`. */
856
+ testIdPrefix?: string;
857
+ /** Called with the fresh user after a successful unlock. */
858
+ onSignedIn: (user: DevicePinUnlockedUser) => void;
859
+ /** Escape hatch → render the normal password/OTP/event-PIN tabs. */
860
+ onUsePassword: () => void;
861
+ }
862
+ /** Themeable device-PIN unlock gate built on `useDevicePinUnlock`. */
863
+ declare function DevicePinUnlockScreen({ client, digits, rememberedUsername, theme: themeProp, labels: labelsProp, testIdPrefix, onSignedIn, onUsePassword, }: Readonly<DevicePinUnlockScreenProps>): ReactElement;
864
+
865
+ /**
866
+ * The shared device-PIN ENROL form — reused by the post-login offer
867
+ * ({@link DevicePinOffer}) and the settings toggle ({@link DevicePinSettingsCard}).
868
+ *
869
+ * Lets the user pick a PIN length (4/6/8), enter + confirm a PIN, and submit via
870
+ * `client.enrollDevicePin` (through {@link useDevicePinEnroll}). On success it
871
+ * calls `onEnrolled`; `onCancel` dismisses without saving. Presentational only —
872
+ * all submit / mismatch / status logic lives in the hook (react-query-free).
873
+ * Styled from the `AuthTheme` token bag.
874
+ */
875
+
876
+ interface DevicePinEnrollFormProps {
877
+ /** The client exposing `enrollDevicePin`. */
878
+ client: DevicePinEnrollCapableClient;
879
+ /** Initial PIN length (defaults to {@link DEVICE_PIN_DEFAULT_DIGITS}). */
880
+ initialDigits?: number;
881
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
882
+ theme?: AuthTheme;
883
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
884
+ labels?: Partial<DevicePinEnrollLabels>;
885
+ /** Prefix applied to every `testID`. */
886
+ testIdPrefix?: string;
887
+ /** Called after a successful enrol. */
888
+ onEnrolled: () => void;
889
+ /** Called when the user cancels without saving. */
890
+ onCancel: () => void;
891
+ }
892
+ /** Themeable device-PIN enrol form built on `useDevicePinEnroll`. */
893
+ declare function DevicePinEnrollForm({ client, initialDigits, theme: themeProp, labels: labelsProp, testIdPrefix, onEnrolled, onCancel, }: Readonly<DevicePinEnrollFormProps>): ReactElement;
894
+
895
+ /**
896
+ * The skippable post-login "Set a PIN for faster sign-in?" offer.
897
+ *
898
+ * Mount on the post-login landing for an authenticated user who does NOT already
899
+ * have a device PIN. It is dismissible and non-blocking: an initial prompt with
900
+ * "Set up a PIN" / "Not now"; tapping accept reveals the shared
901
+ * {@link DevicePinEnrollForm}; on success or skip it calls `onDismiss` so the
902
+ * host can hide it (and remember the dismissal). Self-contained, react-query-free.
903
+ */
904
+
905
+ interface DevicePinOfferProps {
906
+ /** The client exposing `enrollDevicePin`. */
907
+ client: DevicePinEnrollCapableClient;
908
+ /** Initial PIN length offered in the enrol form. */
909
+ initialDigits?: number;
910
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
911
+ theme?: AuthTheme;
912
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
913
+ labels?: Partial<DevicePinEnrollLabels>;
914
+ /** Prefix applied to every `testID`. */
915
+ testIdPrefix?: string;
916
+ /** Called when the offer is finished — accepted+enrolled, or skipped. */
917
+ onDismiss: () => void;
918
+ }
919
+ /** Themeable, skippable post-login device-PIN setup offer. */
920
+ declare function DevicePinOffer({ client, initialDigits, theme: themeProp, labels: labelsProp, testIdPrefix, onDismiss, }: Readonly<DevicePinOfferProps>): ReactElement;
921
+
922
+ /**
923
+ * The authenticated "Quick PIN sign-in" settings control.
924
+ *
925
+ * Mount on an authenticated settings/account surface. Seeded with `initialHasPin`
926
+ * (from `GET /bff/config`); it tracks its own enabled/disabled state thereafter
927
+ * and offers either:
928
+ * - Enable → reveals the shared {@link DevicePinEnrollForm} → `client.enrollDevicePin`;
929
+ * - Disable → `client.disableDevicePin` (revokes the offline token + clears the
930
+ * device record server-side).
931
+ *
932
+ * `onChanged(hasPin)` fires whenever the enabled state flips, so the host can
933
+ * persist / re-fetch. react-query-free: the disable logic lives in
934
+ * {@link useDevicePinDisable}; the enrol logic in the shared form's hook.
935
+ */
936
+
937
+ interface DevicePinSettingsCardProps {
938
+ /** The client exposing both `enrollDevicePin` and `disableDevicePin`. */
939
+ client: DevicePinEnrollCapableClient & DevicePinDisableCapableClient;
940
+ /** Whether this device already has an enrolled PIN (seeded from config). */
941
+ initialHasPin: boolean;
942
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
943
+ theme?: AuthTheme;
944
+ /** Settings-card copy. Partial — unspecified keys fall back to English. */
945
+ labels?: Partial<DevicePinSettingsLabels>;
946
+ /** Enrol-form copy passed through to the embedded form. */
947
+ enrollLabels?: Partial<DevicePinEnrollLabels>;
948
+ /** Prefix applied to every `testID`. */
949
+ testIdPrefix?: string;
950
+ /** Called whenever the enabled state flips (enabled / disabled). */
951
+ onChanged?: (hasPin: boolean) => void;
952
+ }
953
+ /** Themeable authenticated device-PIN enable/disable card. */
954
+ declare function DevicePinSettingsCard({ client, initialHasPin, theme: themeProp, labels: labelsProp, enrollLabels, testIdPrefix, onChanged, }: Readonly<DevicePinSettingsCardProps>): ReactElement;
955
+
956
+ /**
957
+ * The "Sign in with a passkey" button rendered on the unified login surface when
958
+ * the BFF advertises the `passkey` method.
959
+ *
960
+ * A passkey login is a full-page BROWSER NAVIGATION, not a fetch: tapping the
961
+ * button hands off to `/bff/passkey/login`, which drives the WebAuthn ceremony
962
+ * through Keycloak and returns the browser to `returnUrl` (default `'/'`, which
963
+ * routes an authenticated user to their dashboard) with a session cookie set. On
964
+ * a cancelled / failed ceremony the BFF bounces back to `?passkeyError=…`; we
965
+ * surface that as an inline error banner above the button.
966
+ *
967
+ * react-query-free: it is pure `window.location` navigation (see
968
+ * `passkeyNavigation`), so it is safe to mount inside the login route group which
969
+ * has no QueryClient provider.
970
+ */
971
+
972
+ interface PasskeyLoginButtonProps {
973
+ /** Relative app path the BFF returns to on success. Defaults to `'/'`. */
974
+ returnUrl?: string;
975
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
976
+ theme?: AuthTheme;
977
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
978
+ labels?: Partial<PasskeyLoginLabels>;
979
+ /** Prefix applied to every `testID`. */
980
+ testIdPrefix?: string;
981
+ }
982
+ /** Themeable passkey login CTA + error banner. */
983
+ declare function PasskeyLoginButton({ returnUrl, theme: themeProp, labels: labelsProp, testIdPrefix, }: Readonly<PasskeyLoginButtonProps>): ReactElement;
984
+
985
+ /**
986
+ * The authenticated "Passkeys" settings card.
987
+ *
988
+ * Mount on an authenticated settings / account surface (alongside the device-PIN
989
+ * card). Adding a passkey is a full-page BROWSER NAVIGATION to
990
+ * `/bff/passkey/register`: Keycloak re-authenticates the user (password) then
991
+ * runs the WebAuthn credential-creation ceremony, returning the browser to the
992
+ * SAME settings page with `?passkey=registered` appended. On mount we read that
993
+ * param and, when present, show a success line.
994
+ *
995
+ * react-query-free: the add action is pure `window.location` navigation (see
996
+ * `passkeyNavigation`); there is no fetch here. Mirrors `DevicePinSettingsCard`.
997
+ */
998
+
999
+ interface PasskeySettingsCardProps {
1000
+ /**
1001
+ * Relative app path the BFF returns to after registration. Defaults to the
1002
+ * current `window.location.pathname` so the user lands back on this page.
1003
+ */
1004
+ returnUrl?: string;
1005
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
1006
+ theme?: AuthTheme;
1007
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
1008
+ labels?: Partial<PasskeySettingsLabels>;
1009
+ /** Prefix applied to every `testID`. */
1010
+ testIdPrefix?: string;
1011
+ }
1012
+ /** Themeable authenticated passkey add card. */
1013
+ declare function PasskeySettingsCard({ returnUrl, theme: themeProp, labels: labelsProp, testIdPrefix, }: Readonly<PasskeySettingsCardProps>): ReactElement;
1014
+
1015
+ /**
1016
+ * Same-origin client for the user's cross-device login-method preference
1017
+ * (unified-login D5). Unlike the device-PIN client (which targets the BFF's
1018
+ * own `/bff/*` auth endpoints), the preference lives in TenantService and is
1019
+ * reached THROUGH the BFF's YARP proxy:
1020
+ *
1021
+ * - GET `${baseUrl}/bff/api/{tenantsSegment}/api/v1/me/preferences`
1022
+ * → `{ ..., preferredLoginMethod }`
1023
+ * - PUT `${baseUrl}/bff/api/{tenantsSegment}/api/v1/me/login-method-preference`
1024
+ * body `{ method }`
1025
+ *
1026
+ * The BFF attaches the session's Bearer token to the proxied request, so both
1027
+ * calls require an authenticated session (the cookie travels via
1028
+ * `credentials: 'include'`). The PUT carries the `X-BFF-Csrf` header the BFF
1029
+ * anti-forgery middleware requires.
1030
+ *
1031
+ * react-query-FREE: plain `HttpClient` calls with `.then/.catch`. Portable
1032
+ * across every consumer (Expo RN-web apps AND the kefi/poueni surfaces).
1033
+ */
1034
+
1035
+ interface PreferencesClientOptions {
1036
+ /** Runtime-agnostic HTTP transport (wrap native `fetch` with `createFetchHttpClient`). */
1037
+ http: HttpClient;
1038
+ /**
1039
+ * BFF origin. Defaults to `''` (same-origin) — the production wiring. An
1040
+ * explicit origin is only useful for tests or a non-same-origin BFF.
1041
+ */
1042
+ baseUrl?: string;
1043
+ /**
1044
+ * The BFF YARP downstream segment that routes to TenantService. Defaults to
1045
+ * `'tenants'` — the segment every product BFF (katalogos / erevna / kefi)
1046
+ * uses for `tenant-api`.
1047
+ */
1048
+ tenantsSegment?: string;
1049
+ }
1050
+ /**
1051
+ * The narrow client the preferred-method UI depends on. Both methods NEVER
1052
+ * reject — they resolve a benign value on any failure so the settings card and
1053
+ * the post-login cache sync degrade gracefully rather than throwing.
1054
+ */
1055
+ interface PreferredMethodClient {
1056
+ /**
1057
+ * `GET /me/preferences` → the stored preferred login method, or `null` when
1058
+ * the user never picked one (or on any failure).
1059
+ */
1060
+ getPreferredLoginMethod(): Promise<string | null>;
1061
+ /**
1062
+ * `PUT /me/login-method-preference` — set (or clear, with `null`) the stored
1063
+ * preference. Resolves `true` on a 2xx, `false` on anything else.
1064
+ */
1065
+ setPreferredLoginMethod(method: string | null): Promise<boolean>;
1066
+ }
1067
+ /**
1068
+ * Build a same-origin {@link PreferredMethodClient}. With no `baseUrl` this is
1069
+ * the production wiring: same-origin, the session cookie travels automatically.
1070
+ */
1071
+ declare function createPreferencesClient(options: PreferencesClientOptions): PreferredMethodClient;
1072
+
1073
+ /**
1074
+ * The authenticated "Preferred sign-in method" settings card (unified-login D5).
1075
+ *
1076
+ * Mount on an authenticated settings/account surface alongside the device-PIN
1077
+ * and passkey cards. It lets the user pick which of their enabled login methods
1078
+ * they want emphasised on the login screen — stored SERVER-SIDE so the choice
1079
+ * follows them across devices. The login screen reads the device-local mirror
1080
+ * of this value (see `readCachedPreferredMethod` / `<PreferredMethodHint>`).
1081
+ *
1082
+ * `availableMethods` should be the BFF's advertised methods (from
1083
+ * `GET /bff/config` → `useBffLoginConfig().methods`) so the card only offers
1084
+ * methods this product actually supports. A "No preference" option always
1085
+ * appears. react-query-free: the persistence lives in `usePreferredMethodSetting`.
1086
+ */
1087
+
1088
+ interface PreferredMethodSettingsCardProps {
1089
+ /** The client exposing `getPreferredLoginMethod` + `setPreferredLoginMethod`. */
1090
+ client: PreferredMethodClient;
1091
+ /**
1092
+ * The login methods this BFF advertises (lowercase, e.g.
1093
+ * `['password', 'pin', 'passkey']`). Only these are offered, plus the
1094
+ * always-present "No preference" option.
1095
+ */
1096
+ availableMethods: readonly string[];
1097
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
1098
+ theme?: AuthTheme;
1099
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
1100
+ labels?: Partial<PreferredMethodSettingsLabels>;
1101
+ /** Prefix applied to every `testID`. */
1102
+ testIdPrefix?: string;
1103
+ /** Called whenever a save succeeds (`null` = no preference). */
1104
+ onChanged?: (method: string | null) => void;
1105
+ }
1106
+ /** Themeable authenticated preferred-method picker. */
1107
+ declare function PreferredMethodSettingsCard({ client, availableMethods, theme: themeProp, labels: labelsProp, testIdPrefix, onChanged, }: Readonly<PreferredMethodSettingsCardProps>): ReactElement;
1108
+
1109
+ /**
1110
+ * The login-screen "you usually sign in with…" hint (unified-login D5).
1111
+ *
1112
+ * Mount at the top of the login surface. Given the device-local cached
1113
+ * preferred method (`readCachedPreferredMethod()`), it renders a single
1114
+ * reassurance line naming that method. Renders NOTHING for an unknown / absent
1115
+ * method so the login screen stays clean for first-time visitors.
1116
+ *
1117
+ * No network, no state — pure presentation driven by the cached value the host
1118
+ * passes in. (The cache is seeded post-login by `syncPreferredMethodFromServer`.)
1119
+ */
1120
+
1121
+ interface PreferredMethodHintProps {
1122
+ /**
1123
+ * The cached preferred method (`'password'` | `'pin'` | `'passkey'`), or
1124
+ * `null`. Typically `readCachedPreferredMethod()`. An unknown / `null` value
1125
+ * renders nothing.
1126
+ */
1127
+ method: string | null;
1128
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
1129
+ theme?: AuthTheme;
1130
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
1131
+ labels?: Partial<PreferredMethodHintLabels>;
1132
+ /** Prefix applied to the `testID`. */
1133
+ testIdPrefix?: string;
1134
+ }
1135
+ /** Themeable login-screen preferred-method hint. Renders `null` when there is nothing to say. */
1136
+ declare function PreferredMethodHint({ method, theme: themeProp, labels: labelsProp, testIdPrefix, }: Readonly<PreferredMethodHintProps>): ReactElement | null;
1137
+
450
1138
  /**
451
1139
  * React context for the `AuthTheme`.
452
1140
  *
@@ -521,6 +1209,36 @@ declare const AuthTestIds: {
521
1209
  readonly pinInput: "auth-pin-input";
522
1210
  readonly pinSubmitButton: "auth-pin-submit";
523
1211
  readonly pinError: "auth-pin-error";
1212
+ readonly devicePinUnlock: "auth-device-pin-unlock";
1213
+ readonly devicePinUnlockInput: "auth-device-pin-unlock-input";
1214
+ readonly devicePinUnlockSubmit: "auth-device-pin-unlock-submit";
1215
+ readonly devicePinUnlockUsePassword: "auth-device-pin-unlock-use-password";
1216
+ readonly devicePinUnlockError: "auth-device-pin-unlock-error";
1217
+ readonly devicePinEnrollForm: "auth-device-pin-enroll-form";
1218
+ readonly devicePinEnrollLength: "auth-device-pin-enroll-length";
1219
+ readonly devicePinEnrollPin: "auth-device-pin-enroll-pin";
1220
+ readonly devicePinEnrollConfirm: "auth-device-pin-enroll-confirm";
1221
+ readonly devicePinEnrollSubmit: "auth-device-pin-enroll-submit";
1222
+ readonly devicePinEnrollCancel: "auth-device-pin-enroll-cancel";
1223
+ readonly devicePinEnrollError: "auth-device-pin-enroll-error";
1224
+ readonly devicePinOffer: "auth-device-pin-offer";
1225
+ readonly devicePinOfferAccept: "auth-device-pin-offer-accept";
1226
+ readonly devicePinOfferSkip: "auth-device-pin-offer-skip";
1227
+ readonly devicePinSettings: "auth-device-pin-settings";
1228
+ readonly devicePinSettingsStatus: "auth-device-pin-settings-status";
1229
+ readonly devicePinSettingsEnable: "auth-device-pin-settings-enable";
1230
+ readonly devicePinSettingsDisable: "auth-device-pin-settings-disable";
1231
+ readonly devicePinSettingsError: "auth-device-pin-settings-error";
1232
+ readonly passkeyLogin: "auth-passkey-login";
1233
+ readonly passkeyLoginButton: "auth-passkey-login-button";
1234
+ readonly passkeyLoginError: "auth-passkey-login-error";
1235
+ readonly passkeySettings: "auth-passkey-settings";
1236
+ readonly passkeySettingsAdd: "auth-passkey-settings-add";
1237
+ readonly passkeySettingsSuccess: "auth-passkey-settings-success";
1238
+ readonly preferredMethodSettings: "auth-preferred-method-settings";
1239
+ readonly preferredMethodSettingsOption: "auth-preferred-method-settings-option";
1240
+ readonly preferredMethodSettingsError: "auth-preferred-method-settings-error";
1241
+ readonly preferredMethodHint: "auth-preferred-method-hint";
524
1242
  };
525
1243
  /** Apply an optional prefix to a base test ID. */
526
1244
  declare function withTestIdPrefix(baseId: string, prefix?: string): string;
@@ -775,6 +1493,202 @@ interface UsePinLoginResult {
775
1493
  */
776
1494
  declare function usePinLogin(options: UsePinLoginOptions): UsePinLoginResult;
777
1495
 
1496
+ /** What {@link useBffLoginConfig} returns. */
1497
+ interface UseBffLoginConfigResult {
1498
+ /** The parsed login config; the empty fallback until the fetch resolves. */
1499
+ config: BffLoginConfig;
1500
+ /** `true` until the initial `getLoginConfig` call settles. */
1501
+ loading: boolean;
1502
+ }
1503
+ /**
1504
+ * Fetch `getLoginConfig()` once on mount and expose `{ config, loading }`. The
1505
+ * empty fallback config is used until the call resolves and on any failure. The
1506
+ * effect guards against a `setState` after the consumer unmounts mid-request.
1507
+ */
1508
+ declare function useBffLoginConfig(client: LoginConfigCapableClient): UseBffLoginConfigResult;
1509
+
1510
+ /**
1511
+ * The localizable failure states the device-PIN ENROL form can show — shared by
1512
+ * the post-login offer and the settings toggle.
1513
+ */
1514
+ declare const enum DevicePinEnrollErrorKey {
1515
+ /** PIN length wrong or the confirmation didn't match (no network call). */
1516
+ Mismatch = "mismatch",
1517
+ /** The current session is not authenticated (401). */
1518
+ Unauthorized = "unauthorized",
1519
+ /** The session already has a PIN, or is otherwise forbidden (403). */
1520
+ Forbidden = "forbidden",
1521
+ /** The PIN failed server-side validation (400). */
1522
+ InvalidPin = "invalid_pin",
1523
+ /** Any other failure (grant / network / unexpected). */
1524
+ Failed = "failed"
1525
+ }
1526
+
1527
+ interface UseDevicePinEnrollArgs {
1528
+ /** The client exposing `enrollDevicePin`. */
1529
+ client: DevicePinEnrollCapableClient;
1530
+ /** Chosen PIN length (4/6/8). */
1531
+ digits: number;
1532
+ /** Called after a successful enrol so the surface can dismiss / flip state. */
1533
+ onEnrolled: () => void;
1534
+ }
1535
+ interface UseDevicePinEnrollResult {
1536
+ pin: string;
1537
+ confirmPin: string;
1538
+ submitting: boolean;
1539
+ errorKey: DevicePinEnrollErrorKey | null;
1540
+ setPin: (next: string) => void;
1541
+ setConfirmPin: (next: string) => void;
1542
+ submit: () => void;
1543
+ }
1544
+ declare function useDevicePinEnroll({ client, digits, onEnrolled, }: UseDevicePinEnrollArgs): UseDevicePinEnrollResult;
1545
+
1546
+ interface UseDevicePinDisableArgs {
1547
+ /** The client exposing `disableDevicePin`. */
1548
+ client: DevicePinDisableCapableClient;
1549
+ /** Whether this device already has an enrolled PIN (seeded from config). */
1550
+ initialHasPin: boolean;
1551
+ /** Called whenever the enabled state changes (enabled / disabled). */
1552
+ onChanged?: (hasPin: boolean) => void;
1553
+ }
1554
+ interface UseDevicePinDisableResult {
1555
+ /** `true` when the device currently has an enrolled PIN. */
1556
+ hasPin: boolean;
1557
+ /** `true` while a disable call is in flight. */
1558
+ disabling: boolean;
1559
+ /** `true` when the last disable attempt failed. */
1560
+ failed: boolean;
1561
+ /** Run the disable call for the current session. */
1562
+ disable: () => void;
1563
+ /** Flip the card to enabled (called by the host after a successful enrol). */
1564
+ markEnabled: () => void;
1565
+ }
1566
+ declare function useDevicePinDisable({ client, initialHasPin, onChanged, }: UseDevicePinDisableArgs): UseDevicePinDisableResult;
1567
+
1568
+ /**
1569
+ * Shared device-PIN constants for the unlock + enrol surfaces.
1570
+ *
1571
+ * The allowed PIN lengths (4 / 6 / 8) mirror the BFF's accepted `digits` values
1572
+ * (`POST /bff/pin/enroll`). Centralised here so neither the enrol form nor the
1573
+ * unlock screen carries magic numbers, and so the default length is defined once.
1574
+ */
1575
+ /** The PIN lengths the BFF accepts on enrol (`digits` ∈ {4, 6, 8}). */
1576
+ declare const DEVICE_PIN_ALLOWED_DIGITS: readonly number[];
1577
+ /** Default PIN length offered when the BFF reports no stored `pinDigits`. */
1578
+ declare const DEVICE_PIN_DEFAULT_DIGITS = 4;
1579
+ /** Guard: is `value` one of the BFF-accepted PIN lengths? */
1580
+ declare function isAllowedPinDigits(value: number): boolean;
1581
+
1582
+ /**
1583
+ * Navigation helpers for the passkey (WebAuthn) BFF endpoints.
1584
+ *
1585
+ * Unlike the device-PIN client (which is plain `fetch`), the passkey login and
1586
+ * registration ceremonies are NOT fetches — they are full-page BROWSER
1587
+ * NAVIGATIONS. The BFF (`Bff.AspNetCore` 1.3.x) drives the WebAuthn ceremony
1588
+ * through Keycloak's hosted pages, then redirects the browser back:
1589
+ * - login: `/bff/passkey/login?returnUrl=<rel>` → on success the browser
1590
+ * lands on `returnUrl` with the session cookie already set;
1591
+ * - registration: `/bff/passkey/register?returnUrl=<rel>` → Keycloak re-auths
1592
+ * the user (password) then runs the credential-creation ceremony, returning
1593
+ * the browser to `returnUrl?passkey=registered`;
1594
+ * - failure / cancel: the BFF redirects to `<login>?passkeyError=cancelled` or
1595
+ * `<login>?passkeyError=failed`.
1596
+ *
1597
+ * react-query-FREE by construction: there is NO fetch here at all — every
1598
+ * function is a pure `window.location` read or `window.location.assign`. The
1599
+ * login route group has no QueryClient provider, so the passkey login button
1600
+ * (which lives there) MUST avoid any react-query primitive; navigation satisfies
1601
+ * that trivially.
1602
+ *
1603
+ * All reads are SSR-safe: when `window` is undefined (server render / native)
1604
+ * they return a benign default rather than throwing.
1605
+ */
1606
+ /** The recognised passkey error codes the BFF sends back via `?passkeyError=`. */
1607
+ type PasskeyErrorCode = 'cancelled' | 'failed';
1608
+ /**
1609
+ * Start the passkey LOGIN ceremony by navigating the browser to the BFF's
1610
+ * `/bff/passkey/login` endpoint. The BFF redirects through Keycloak's WebAuthn
1611
+ * ceremony and, on success, returns the browser to `returnUrl` with a session
1612
+ * cookie set. No-op off-web (no `window`). `returnUrl` should be a relative
1613
+ * app path (e.g. `'/'`).
1614
+ */
1615
+ declare function startPasskeyLogin(returnUrl: string): void;
1616
+ /**
1617
+ * Start the passkey REGISTRATION ceremony by navigating the browser to the BFF's
1618
+ * `/bff/passkey/register` endpoint. Requires an authenticated session; Keycloak
1619
+ * re-authenticates the user (password) then runs the credential-creation
1620
+ * ceremony, returning the browser to `returnUrl?passkey=registered`. No-op
1621
+ * off-web. `returnUrl` should be a relative app path so the user lands back on
1622
+ * the same settings surface.
1623
+ */
1624
+ declare function startPasskeyRegistration(returnUrl: string): void;
1625
+ /**
1626
+ * Read the `?passkeyError=` query param the BFF appends on a failed / cancelled
1627
+ * ceremony. Returns `'cancelled'` or `'failed'` for the two known values, or
1628
+ * `null` for an absent / unknown value. SSR-safe (returns `null` off-web).
1629
+ */
1630
+ declare function readPasskeyError(): PasskeyErrorCode | null;
1631
+ /**
1632
+ * `true` when the browser has just returned from a successful passkey
1633
+ * registration, i.e. the `?passkey=registered` query param is present. SSR-safe
1634
+ * (returns `false` off-web).
1635
+ */
1636
+ declare function readPasskeyRegistered(): boolean;
1637
+
1638
+ /**
1639
+ * Device-local cache of the user's preferred login method (unified-login D5).
1640
+ *
1641
+ * The preference is owned server-side (TenantService UserPreference), but the
1642
+ * LOGIN screen needs it BEFORE the user is authenticated — when no server call
1643
+ * is possible. So we cache it in `localStorage`, seeded after every successful
1644
+ * login (see {@link syncPreferredMethodFromServer}). A user who sets "passkey"
1645
+ * on device A will, after their first login on device B, see device B's login
1646
+ * screen prefer passkey too — cross-device sync with one-login lag.
1647
+ *
1648
+ * SSR-safe: every function no-ops / returns `null` when `localStorage` is
1649
+ * unavailable (server render, native, privacy modes that throw on access).
1650
+ */
1651
+ /**
1652
+ * Read the cached preferred login method, or `null` when none is cached / the
1653
+ * store is unavailable. Cheap + synchronous so the login screen can call it on
1654
+ * render.
1655
+ */
1656
+ declare function readCachedPreferredMethod(): string | null;
1657
+ /**
1658
+ * Write (or, with `null`, clear) the cached preferred login method. No-op when
1659
+ * the store is unavailable. Never throws.
1660
+ */
1661
+ declare function cachePreferredMethod(method: string | null): void;
1662
+ /** Clear the cached preferred login method (e.g. on logout). Never throws. */
1663
+ declare function clearCachedPreferredMethod(): void;
1664
+
1665
+ /**
1666
+ * Fetch the server-stored preferred login method and mirror it into the
1667
+ * device-local cache. Resolves to the value written (or `null`). Safe to
1668
+ * `void` — it swallows all errors.
1669
+ */
1670
+ declare function syncPreferredMethodFromServer(client: PreferredMethodClient): Promise<string | null>;
1671
+
1672
+ interface UsePreferredMethodSettingArgs {
1673
+ /** The client exposing `getPreferredLoginMethod` + `setPreferredLoginMethod`. */
1674
+ client: PreferredMethodClient;
1675
+ /** Called whenever a save succeeds (the new value, `null` = no preference). */
1676
+ onChanged?: (method: string | null) => void;
1677
+ }
1678
+ interface UsePreferredMethodSettingResult {
1679
+ /** `true` until the initial server load resolves. */
1680
+ loading: boolean;
1681
+ /** `true` while a selection is being persisted. */
1682
+ saving: boolean;
1683
+ /** `true` when the last save failed (cleared on the next `select`). */
1684
+ failed: boolean;
1685
+ /** The currently-selected method, or `null` for "no preference". */
1686
+ selected: string | null;
1687
+ /** Optimistically select a method (`null` clears the preference) and persist it. */
1688
+ select: (method: string | null) => void;
1689
+ }
1690
+ declare function usePreferredMethodSetting({ client, onChanged, }: UsePreferredMethodSettingArgs): UsePreferredMethodSettingResult;
1691
+
778
1692
  /**
779
1693
  * `createBffAuthClient` — the one-line wiring of a same-origin `BffAuthClient`.
780
1694
  *
@@ -910,4 +1824,4 @@ declare function validatePasswordPolicy(password: string): PasswordPolicyError[]
910
1824
  /** `true` when the password satisfies every policy rule. */
911
1825
  declare function isPasswordValid(password: string): boolean;
912
1826
 
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 };
1827
+ export { AuthTestIds, type AuthTheme, type AuthThemeColors, AuthThemeProvider, type AuthThemeProviderProps, type AuthThemeRadii, type AuthThemeSpacing, type AuthThemeTypography, BffAuthStatus, type BffDeviceState, type BffLoginConfig, type CreateBffAuthClientOptions, DEFAULT_DEVICE_PIN_ENROLL_LABELS, DEFAULT_DEVICE_PIN_SETTINGS_LABELS, DEFAULT_DEVICE_PIN_UNLOCK_LABELS, DEFAULT_FORGOT_PASSWORD_FIELDS_LABELS, DEFAULT_FORGOT_PASSWORD_LABELS, DEFAULT_LOGIN_LABELS, DEFAULT_OTP_LABELS, DEFAULT_PASSKEY_LOGIN_LABELS, DEFAULT_PASSKEY_SETTINGS_LABELS, DEFAULT_PIN_LABELS, DEFAULT_PREFERRED_METHOD_HINT_LABELS, DEFAULT_PREFERRED_METHOD_SETTINGS_LABELS, DEFAULT_RESET_PASSWORD_LABELS, DEVICE_PIN_ALLOWED_DIGITS, DEVICE_PIN_DEFAULT_DIGITS, type DevicePinCapableClient, type DevicePinDisableCapableClient, type DevicePinEnrollCapableClient, DevicePinEnrollErrorKey, DevicePinEnrollForm, type DevicePinEnrollFormProps, type DevicePinEnrollLabels, type DevicePinEnrollResult, DevicePinErrorKey, DevicePinInput, type DevicePinInputProps, DevicePinLengthPicker, type DevicePinLengthPickerProps, DevicePinOffer, type DevicePinOfferProps, DevicePinSettingsCard, type DevicePinSettingsCardProps, type DevicePinSettingsLabels, type DevicePinUnlockCapableClient, type DevicePinUnlockLabels, type DevicePinUnlockResult, DevicePinUnlockScreen, type DevicePinUnlockScreenProps, type DevicePinUnlockedUser, ForgotPasswordFields, type ForgotPasswordFieldsLabels, type ForgotPasswordFieldsProps, ForgotPasswordForm, type ForgotPasswordFormLabels, type ForgotPasswordFormProps, type LoginConfigCapableClient, LoginForm, type LoginFormLabels, type LoginFormProps, OtpForm, type OtpFormLabels, type OtpFormProps, OtpLoginStep, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, type PasskeyErrorCode, PasskeyLoginButton, type PasskeyLoginButtonProps, type PasskeyLoginLabels, PasskeySettingsCard, type PasskeySettingsCardProps, type PasskeySettingsLabels, PasswordPolicyError, PinForm, type PinFormLabels, type PinFormProps, type PreferencesClientOptions, type PreferredMethodClient, PreferredMethodHint, type PreferredMethodHintLabels, type PreferredMethodHintProps, PreferredMethodSettingsCard, type PreferredMethodSettingsCardProps, type PreferredMethodSettingsLabels, ResetPasswordError, ResetPasswordForm, type ResetPasswordFormLabels, type ResetPasswordFormProps, type RoleRoute, type RoleRouteTable, type UseBffAuthOptions, type UseBffAuthResult, type UseBffForgotPasswordOptions, type UseBffLoginConfigResult, type UseBffResetPasswordOptions, type UseDevicePinDisableArgs, type UseDevicePinDisableResult, type UseDevicePinEnrollArgs, type UseDevicePinEnrollResult, type UseDevicePinUnlockArgs, type UseDevicePinUnlockResult, type UseForgotPasswordSubmitArgs, type UseForgotPasswordSubmitResult, type UseOtpLoginOptions, type UseOtpLoginResult, type UsePinLoginOptions, type UsePinLoginResult, type UsePreferredMethodSettingArgs, type UsePreferredMethodSettingResult, type UseResetPasswordFormArgs, type UseResetPasswordFormResult, cachePreferredMethod, clearCachedPreferredMethod, collectUserRoles, createBffAuthClient, createPreferencesClient, defaultAuthTheme, isAllowedPinDigits, isPasswordValid, isValidForgotPasswordEmail, readCachedPreferredMethod, readPasskeyError, readPasskeyRegistered, resolvePostLoginRoute, startPasskeyLogin, startPasskeyRegistration, syncPreferredMethodFromServer, useAuthTheme, useBffAuth, useBffForgotPassword, useBffLoginConfig, useBffResetPassword, useDevicePinDisable, useDevicePinEnroll, useDevicePinUnlock, useForgotPasswordSubmit, useOtpLogin, usePinLogin, usePreferredMethodSetting, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };