@dloizides/auth-web 1.9.1 → 1.11.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
@@ -391,6 +391,21 @@ interface PreferredMethodHintLabels {
391
391
  /** Full sentence shown when the cached preference is passkey. */
392
392
  passkey: string;
393
393
  }
394
+ /** Strings rendered by `<DemoCredentialsHint>` — the public-demo credentials panel. */
395
+ interface DemoCredentialsHintLabels {
396
+ /** Panel heading (e.g. "Demo access"). */
397
+ title: string;
398
+ /** Copy under the heading explaining what the demo account is. */
399
+ description: string;
400
+ /** Row label for the demo username. */
401
+ usernameLabel: string;
402
+ /** Row label for the demo password. */
403
+ passwordLabel: string;
404
+ /** The "use these credentials" button that fills the login form. */
405
+ useCredentials: string;
406
+ /** Accessibility hint for that button. */
407
+ useCredentialsHint: string;
408
+ }
394
409
  /** Strings rendered by `<ResetPasswordForm>`. */
395
410
  interface ResetPasswordFormLabels {
396
411
  title: string;
@@ -424,6 +439,7 @@ declare const DEFAULT_PASSKEY_LOGIN_LABELS: PasskeyLoginLabels;
424
439
  declare const DEFAULT_PASSKEY_SETTINGS_LABELS: PasskeySettingsLabels;
425
440
  declare const DEFAULT_PREFERRED_METHOD_SETTINGS_LABELS: PreferredMethodSettingsLabels;
426
441
  declare const DEFAULT_PREFERRED_METHOD_HINT_LABELS: PreferredMethodHintLabels;
442
+ declare const DEFAULT_DEMO_CREDENTIALS_HINT_LABELS: DemoCredentialsHintLabels;
427
443
  declare const DEFAULT_RESET_PASSWORD_LABELS: ResetPasswordFormLabels;
428
444
 
429
445
  /**
@@ -467,9 +483,27 @@ interface LoginFormProps {
467
483
  * is byte-identical to before.
468
484
  */
469
485
  chromeless?: boolean;
486
+ /**
487
+ * Seeds the username field's INITIAL value. The field stays fully editable and
488
+ * clearable afterwards — this is a starting value, not a controlled one, so a
489
+ * real account holder can always wipe it and type their own.
490
+ */
491
+ initialUsername?: string;
492
+ /** Seeds the password field's initial value. Editable + clearable, as above. */
493
+ initialPassword?: string;
494
+ /**
495
+ * Re-seed signal. Changing it re-applies `initialUsername` / `initialPassword`
496
+ * to the fields even after the user has edited them — needed because React
497
+ * ignores a `useState` initial value on re-render, so changing the `initial*`
498
+ * props alone does nothing once mounted.
499
+ *
500
+ * Most apps should not manage this by hand: `useDemoCredentials` returns a
501
+ * `loginFormProps` bag carrying all three, ready to spread.
502
+ */
503
+ prefillNonce?: number;
470
504
  }
471
505
  /** Themeable password-login form built on `useBffAuth`. */
472
- declare function LoginForm({ client, theme: themeProp, labels: labelsProp, onSuccess, onForgotPassword, onSignUp, testIdPrefix, chromeless, }: Readonly<LoginFormProps>): ReactElement;
506
+ declare function LoginForm({ client, theme: themeProp, labels: labelsProp, onSuccess, onForgotPassword, onSignUp, testIdPrefix, chromeless, initialUsername, initialPassword, prefillNonce, }: Readonly<LoginFormProps>): ReactElement;
473
507
 
474
508
  /**
475
509
  * `<ForgotPasswordForm>` — the ready-made, themeable "request a reset link"
@@ -1211,6 +1245,67 @@ interface PreferredMethodHintProps {
1211
1245
  /** Themeable login-screen preferred-method hint. Renders `null` when there is nothing to say. */
1212
1246
  declare function PreferredMethodHint({ method, theme: themeProp, labels: labelsProp, testIdPrefix, }: Readonly<PreferredMethodHintProps>): ReactElement | null;
1213
1247
 
1248
+ /**
1249
+ * Public demo-credential contracts.
1250
+ *
1251
+ * A "demo credential" is a REAL account that a product publishes openly so a
1252
+ * prospective evaluator can sign in without a sign-up step. It is not a bypass:
1253
+ * the credentials travel the ordinary login path (same BFF, same Keycloak, same
1254
+ * session cookie, same roles). The package only makes them *discoverable* and
1255
+ * *one-click fillable* — it never authenticates anything itself.
1256
+ */
1257
+ /** A publishable demo username + password pair. */
1258
+ interface DemoCredentials {
1259
+ /** The demo account's username. */
1260
+ username: string;
1261
+ /** The demo account's password. */
1262
+ password: string;
1263
+ }
1264
+
1265
+ /**
1266
+ * `<DemoCredentialsHint>` — the public-demo credentials panel.
1267
+ *
1268
+ * Mount on the login surface of a product with an OPEN demo account whose
1269
+ * credentials are already published (typically on the marketing site). It shows
1270
+ * the username and password and offers a one-tap "use these credentials" action
1271
+ * that fills the form, so a non-technical evaluator never has to copy-paste a
1272
+ * password between two tabs to see the product.
1273
+ *
1274
+ * It authenticates nothing. It fills fields; `<LoginForm>` then submits them
1275
+ * down the ordinary path — same BFF, same Keycloak, same session, same roles.
1276
+ * There is no bypass here and no credential in this package: the host supplies
1277
+ * the pair, and the demo account's blast radius is whatever the IdP grants it.
1278
+ *
1279
+ * Fails closed: no credentials (or a half-configured pair) renders `null` — zero
1280
+ * DOM — so a misconfigured env var can never publish an empty demo panel.
1281
+ *
1282
+ * Compose it as a child of `<AuthScreen>`, above a `chromeless` `<LoginForm>`;
1283
+ * `useDemoCredentials` wires the two together. Read-only mode (no `onUse`) drops
1284
+ * the button and just displays the credentials.
1285
+ */
1286
+
1287
+ interface DemoCredentialsHintProps {
1288
+ /**
1289
+ * The demo credentials to advertise. `undefined` — or a pair with a blank
1290
+ * username or password — renders nothing at all.
1291
+ */
1292
+ credentials?: DemoCredentials;
1293
+ /** Explicit theme; overrides any `<AuthThemeProvider>`. */
1294
+ theme?: AuthTheme;
1295
+ /** Localised copy. Partial — unspecified keys fall back to English defaults. */
1296
+ labels?: Partial<DemoCredentialsHintLabels>;
1297
+ /** Prefix applied to every `testID`. */
1298
+ testIdPrefix?: string;
1299
+ /**
1300
+ * Called with the resolved credentials when the user taps "use these
1301
+ * credentials". Omit to render the panel read-only (no button) — the values
1302
+ * are still selectable, so a visitor can copy them by hand.
1303
+ */
1304
+ onUse?: (credentials: DemoCredentials) => void;
1305
+ }
1306
+ /** Themeable public-demo credentials panel. Renders `null` when there is no demo. */
1307
+ declare function DemoCredentialsHint({ credentials, theme: themeProp, labels: labelsProp, testIdPrefix, onUse, }: Readonly<DemoCredentialsHintProps>): ReactElement | null;
1308
+
1214
1309
  /**
1215
1310
  * React context for the `AuthTheme`.
1216
1311
  *
@@ -1316,6 +1411,10 @@ declare const AuthTestIds: {
1316
1411
  readonly preferredMethodSettingsOption: "auth-preferred-method-settings-option";
1317
1412
  readonly preferredMethodSettingsError: "auth-preferred-method-settings-error";
1318
1413
  readonly preferredMethodHint: "auth-preferred-method-hint";
1414
+ readonly demoCredentialsHint: "auth-demo-credentials-hint";
1415
+ readonly demoCredentialsUsername: "auth-demo-credentials-username";
1416
+ readonly demoCredentialsPassword: "auth-demo-credentials-password";
1417
+ readonly demoCredentialsUseButton: "auth-demo-credentials-use";
1319
1418
  };
1320
1419
  /** Apply an optional prefix to a base test ID. */
1321
1420
  declare function withTestIdPrefix(baseId: string, prefix?: string): string;
@@ -1818,6 +1917,77 @@ interface UsePreferredMethodSettingResult {
1818
1917
  }
1819
1918
  declare function usePreferredMethodSetting({ client, onChanged, }: UsePreferredMethodSettingArgs): UsePreferredMethodSettingResult;
1820
1919
 
1920
+ /** Options for {@link useDemoCredentials}. */
1921
+ interface UseDemoCredentialsOptions {
1922
+ /**
1923
+ * The demo credentials to advertise. Omit (or pass a half-configured pair) and
1924
+ * the whole surface disappears: the hint renders nothing and the form is never
1925
+ * seeded. See `resolveDemoCredentials`.
1926
+ */
1927
+ credentials?: DemoCredentials;
1928
+ /**
1929
+ * When `true` the form arrives ALREADY filled, so signing in as the demo user
1930
+ * is a single tap on "Sign in" — the hint panel then explains why the fields
1931
+ * are populated and re-fills them if the visitor clears them.
1932
+ *
1933
+ * Defaults to `false`: the fields start empty and the visitor opts in by
1934
+ * tapping "Use these credentials". Prefer the default unless the page is a
1935
+ * genuinely public demo — a login form that arrives pre-filled with someone
1936
+ * else's credentials is confusing on a page real account holders also use.
1937
+ */
1938
+ prefillOnMount?: boolean;
1939
+ }
1940
+ /** The `<LoginForm>` props that carry the demo prefill. Spread onto the form. */
1941
+ interface DemoCredentialsLoginFormProps {
1942
+ initialUsername?: string;
1943
+ initialPassword?: string;
1944
+ prefillNonce: number;
1945
+ }
1946
+ /** The `<DemoCredentialsHint>` props that carry the credentials + fill action. */
1947
+ interface DemoCredentialsHintWiring {
1948
+ credentials?: DemoCredentials;
1949
+ onUse: (credentials: DemoCredentials) => void;
1950
+ }
1951
+ /** What {@link useDemoCredentials} hands back. */
1952
+ interface UseDemoCredentialsResult {
1953
+ /** Spread onto `<LoginForm>`. */
1954
+ loginFormProps: DemoCredentialsLoginFormProps;
1955
+ /** Spread onto `<DemoCredentialsHint>`. */
1956
+ hintProps: DemoCredentialsHintWiring;
1957
+ }
1958
+ /** Wire a demo-credentials hint to a login form. Returns two prop bags to spread. */
1959
+ declare function useDemoCredentials({ credentials, prefillOnMount, }?: Readonly<UseDemoCredentialsOptions>): UseDemoCredentialsResult;
1960
+
1961
+ /**
1962
+ * `resolveDemoCredentials` — the single fail-closed gate for the demo surface.
1963
+ *
1964
+ * Every demo affordance in the package (the `<DemoCredentialsHint>` panel and
1965
+ * the `useDemoCredentials` wiring hook) routes its input through this function,
1966
+ * so "should we advertise a demo account at all?" is decided in exactly ONE
1967
+ * place and always the same way.
1968
+ *
1969
+ * Fail closed, deliberately. The realistic source of these values is an env var
1970
+ * (`process.env.EXPO_PUBLIC_DEMO_USERNAME`), which is `string | undefined` at
1971
+ * runtime no matter what the type says — a consumer writing `username: env.X!`
1972
+ * hands us `undefined` and TypeScript never notices. A half-configured demo must
1973
+ * render NOTHING rather than an empty panel inviting a client to sign in with
1974
+ * blank credentials, so anything that is not a genuinely non-empty string pair
1975
+ * resolves to `null`.
1976
+ */
1977
+
1978
+ /**
1979
+ * Narrow an unvalidated demo-credential pair to a usable one, or `null`.
1980
+ *
1981
+ * Returns `null` for: `undefined` / `null`, a non-string field (runtime junk
1982
+ * from an unset env var), a blank/whitespace-only username, and an empty
1983
+ * password. Otherwise returns a NEW pair with the username trimmed.
1984
+ *
1985
+ * The password is deliberately NOT trimmed: leading/trailing whitespace can be
1986
+ * a legitimate part of a password, and silently altering it would turn a
1987
+ * working credential into a mystery login failure.
1988
+ */
1989
+ declare function resolveDemoCredentials(credentials?: DemoCredentials | null): DemoCredentials | null;
1990
+
1821
1991
  /**
1822
1992
  * `createBffAuthClient` — the one-line wiring of a same-origin `BffAuthClient`.
1823
1993
  *
@@ -1925,6 +2095,97 @@ declare function resolvePostLoginRoute(user: BffUser, table: RoleRouteTable): st
1925
2095
  */
1926
2096
  declare function resolveAccessibleRoutes(user: BffUser, table: RoleRouteTable): RoleRoute[];
1927
2097
 
2098
+ /**
2099
+ * `performBffLogout` — the shared logout sequencer.
2100
+ *
2101
+ * Every app hand-rolled its own logout ordering inside its `AuthProvider`, and
2102
+ * the orderings disagreed. Two of them (zygos-web, agora-web) navigated FIRST
2103
+ * and called the BFF second:
2104
+ *
2105
+ * ```ts
2106
+ * redirectTo('/(auth)/login'); // window.location.replace() → document unload
2107
+ * await bffAuthClient.logout(); // races the unload — may never be sent
2108
+ * ```
2109
+ *
2110
+ * That is wrong twice over:
2111
+ *
2112
+ * 1. **The BFF call can be cancelled.** A document navigation kills in-flight
2113
+ * fetches, so `POST /bff/logout` may never reach the server: the SPA looks
2114
+ * logged out while the httpOnly session cookie stays ALIVE. The `catch`
2115
+ * logged it "non-fatal", so the defect was silent.
2116
+ * 2. **The IdP session survives.** `BffAuthClient.logout()` ends the *identity
2117
+ * provider's* browser session by doing a top-level navigation to the IdP's
2118
+ * RP-initiated logout URL (only a real navigation carries the IdP's own
2119
+ * cookie — a back-channel POST cannot clear it). An app that navigates to
2120
+ * its own login route first pre-empts that navigation, so the IdP still
2121
+ * considers the user signed in and the next authorize round-trip silently
2122
+ * re-authenticates them. That is the "sign-out that signs you back in" bug
2123
+ * the client's own docs warn about.
2124
+ *
2125
+ * This helper owns the whole sequence so neither can be reintroduced per-app:
2126
+ *
2127
+ * 1. `onClearSession()` — drop local state immediately so the UI reflects the
2128
+ * sign-out right away (perceived performance; no spinner needed).
2129
+ * 2. `await client.logout({ redirect: false })` — let the network call SETTLE,
2130
+ * and take the IdP logout URL rather than letting the client navigate mid-
2131
+ * sequence. Errors are captured, never rethrown.
2132
+ * 3. Navigate LAST, exactly once, when nothing is in flight to cancel:
2133
+ * - an IdP logout URL → a top-level navigation to the IdP, which ends that
2134
+ * session and bounces the user back. `onRedirect` is deliberately NOT
2135
+ * called: two competing navigations would race, and the IdP round-trip
2136
+ * already returns the user to the app.
2137
+ * - otherwise (ROPC / OTP / device-PIN logins never had an IdP cookie, so
2138
+ * the URL is `null`) → `onRedirect()`, the app's own login route.
2139
+ *
2140
+ * Step 3 runs even when step 2 rejects: a BFF that is down must not trap the
2141
+ * user on a page they asked to leave. The local session is already gone, so the
2142
+ * client-side sign-out holds regardless.
2143
+ *
2144
+ * Headless and framework-free: it takes callbacks, so an app can pair it with
2145
+ * Redux, a context, expo-router, or `window.location` without this package
2146
+ * knowing about any of them.
2147
+ */
2148
+ /**
2149
+ * The only part of a `BffAuthClient` this helper needs. Structural, so it is
2150
+ * trivially fakeable in tests and does not couple to the full client surface.
2151
+ */
2152
+ interface BffLogoutClient {
2153
+ /**
2154
+ * `POST /bff/logout` — ends the server-side session. With `redirect: false`
2155
+ * it returns the IdP's RP-initiated logout URL (or `null` when the session
2156
+ * never involved an IdP browser cookie) instead of navigating there itself.
2157
+ */
2158
+ logout: (options?: {
2159
+ redirect?: boolean;
2160
+ }) => Promise<string | null>;
2161
+ }
2162
+ /** Callbacks + client for {@link performBffLogout}. */
2163
+ interface PerformBffLogoutOptions {
2164
+ /** The BFF auth client whose `logout()` ends the server-side session. */
2165
+ client: BffLogoutClient;
2166
+ /** Drop local session state. Runs BEFORE the network call. Optional. */
2167
+ onClearSession?: () => void;
2168
+ /**
2169
+ * Navigate to the app's logged-out route. Runs only AFTER the network call
2170
+ * settles, and only when there is no IdP logout URL to honour. Optional.
2171
+ */
2172
+ onRedirect?: () => void;
2173
+ /** Observe a failed logout step (e.g. to log it). Never rethrown. Optional. */
2174
+ onError?: (error: Error) => void;
2175
+ /**
2176
+ * Test seam: perform the top-level navigation to the IdP logout URL. Defaults
2177
+ * to `window.location.assign`. Must be a real navigation — a `fetch` here
2178
+ * would not carry the IdP cookie and would silently fail to end the session.
2179
+ */
2180
+ navigateToIdp?: (url: string) => void;
2181
+ }
2182
+ /**
2183
+ * End the session: clear locally, await the BFF, then navigate exactly once.
2184
+ *
2185
+ * Never rejects — callers may `void` it safely.
2186
+ */
2187
+ declare function performBffLogout(options: Readonly<PerformBffLogoutOptions>): Promise<void>;
2188
+
1928
2189
  /**
1929
2190
  * The discrete ways a password can fail the shared client-side policy.
1930
2191
  *
@@ -1970,4 +2231,4 @@ declare function validatePasswordPolicy(password: string): PasswordPolicyError[]
1970
2231
  /** `true` when the password satisfies every policy rule. */
1971
2232
  declare function isPasswordValid(password: string): boolean;
1972
2233
 
1973
- export { AuthScreen, type AuthScreenProps, AuthTestIds, type AuthTheme, type AuthThemeColors, AuthThemeProvider, type AuthThemeProviderProps, type AuthThemeRadii, type AuthThemeSpacing, type AuthThemeTypography, BffAuthStatus, type BffDeviceState, type BffLoginConfig, type CreateBffAuthClientOptions, DEFAULT_AUTH_CARD_MAX_WIDTH, 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, PasskeyCeremonyMode, PasskeyCeremonyStatus, 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 UsePasskeyCeremonyOptions, type UsePasskeyCeremonyResult, type UsePinLoginOptions, type UsePinLoginResult, type UsePreferredMethodSettingArgs, type UsePreferredMethodSettingResult, type UseResetPasswordFormArgs, type UseResetPasswordFormResult, cachePreferredMethod, clearCachedPreferredMethod, collectUserRoles, createBffAuthClient, createPreferencesClient, defaultAuthTheme, isAllowedPinDigits, isPasswordValid, isValidForgotPasswordEmail, readCachedPreferredMethod, readPasskeyError, readPasskeyRegistered, resolveAccessibleRoutes, resolvePostLoginRoute, startPasskeyLogin, startPasskeyRegistration, syncPreferredMethodFromServer, useAuthTheme, useBffAuth, useBffForgotPassword, useBffLoginConfig, useBffResetPassword, useDevicePinDisable, useDevicePinEnroll, useDevicePinUnlock, useForgotPasswordSubmit, useOtpLogin, usePasskeyCeremony, usePinLogin, usePreferredMethodSetting, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };
2234
+ export { AuthScreen, type AuthScreenProps, AuthTestIds, type AuthTheme, type AuthThemeColors, AuthThemeProvider, type AuthThemeProviderProps, type AuthThemeRadii, type AuthThemeSpacing, type AuthThemeTypography, BffAuthStatus, type BffDeviceState, type BffLoginConfig, type BffLogoutClient, type CreateBffAuthClientOptions, DEFAULT_AUTH_CARD_MAX_WIDTH, DEFAULT_DEMO_CREDENTIALS_HINT_LABELS, 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 DemoCredentials, DemoCredentialsHint, type DemoCredentialsHintLabels, type DemoCredentialsHintProps, type DemoCredentialsHintWiring, type DemoCredentialsLoginFormProps, 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, PasskeyCeremonyMode, PasskeyCeremonyStatus, type PasskeyErrorCode, PasskeyLoginButton, type PasskeyLoginButtonProps, type PasskeyLoginLabels, PasskeySettingsCard, type PasskeySettingsCardProps, type PasskeySettingsLabels, PasswordPolicyError, type PerformBffLogoutOptions, 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 UseDemoCredentialsOptions, type UseDemoCredentialsResult, type UseDevicePinDisableArgs, type UseDevicePinDisableResult, type UseDevicePinEnrollArgs, type UseDevicePinEnrollResult, type UseDevicePinUnlockArgs, type UseDevicePinUnlockResult, type UseForgotPasswordSubmitArgs, type UseForgotPasswordSubmitResult, type UseOtpLoginOptions, type UseOtpLoginResult, type UsePasskeyCeremonyOptions, type UsePasskeyCeremonyResult, type UsePinLoginOptions, type UsePinLoginResult, type UsePreferredMethodSettingArgs, type UsePreferredMethodSettingResult, type UseResetPasswordFormArgs, type UseResetPasswordFormResult, cachePreferredMethod, clearCachedPreferredMethod, collectUserRoles, createBffAuthClient, createPreferencesClient, defaultAuthTheme, isAllowedPinDigits, isPasswordValid, isValidForgotPasswordEmail, performBffLogout, readCachedPreferredMethod, readPasskeyError, readPasskeyRegistered, resolveAccessibleRoutes, resolveDemoCredentials, resolvePostLoginRoute, startPasskeyLogin, startPasskeyRegistration, syncPreferredMethodFromServer, useAuthTheme, useBffAuth, useBffForgotPassword, useBffLoginConfig, useBffResetPassword, useDemoCredentials, useDevicePinDisable, useDevicePinEnroll, useDevicePinUnlock, useForgotPasswordSubmit, useOtpLogin, usePasskeyCeremony, usePinLogin, usePreferredMethodSetting, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };
package/dist/index.js CHANGED
@@ -262,6 +262,14 @@ var DEFAULT_PREFERRED_METHOD_HINT_LABELS = {
262
262
  pin: "You usually sign in with your quick PIN on this account.",
263
263
  passkey: "You usually sign in with a passkey."
264
264
  };
265
+ var DEFAULT_DEMO_CREDENTIALS_HINT_LABELS = {
266
+ title: "Demo access",
267
+ description: "Explore the full product with our shared demo account.",
268
+ usernameLabel: "Username",
269
+ passwordLabel: "Password",
270
+ useCredentials: "Use these credentials",
271
+ useCredentialsHint: "Fill the sign-in form with the demo username and password"
272
+ };
265
273
  var DEFAULT_RESET_PASSWORD_LABELS = {
266
274
  title: "Reset password",
267
275
  description: "Choose a new password for your account.",
@@ -350,7 +358,12 @@ var AuthTestIds = {
350
358
  preferredMethodSettingsOption: "auth-preferred-method-settings-option",
351
359
  preferredMethodSettingsError: "auth-preferred-method-settings-error",
352
360
  // Preferred-method login-screen hint
353
- preferredMethodHint: "auth-preferred-method-hint"
361
+ preferredMethodHint: "auth-preferred-method-hint",
362
+ // Public-demo credentials panel
363
+ demoCredentialsHint: "auth-demo-credentials-hint",
364
+ demoCredentialsUsername: "auth-demo-credentials-username",
365
+ demoCredentialsPassword: "auth-demo-credentials-password",
366
+ demoCredentialsUseButton: "auth-demo-credentials-use"
354
367
  };
355
368
  function withTestIdPrefix(baseId, prefix) {
356
369
  return prefix === void 0 || prefix === "" ? baseId : `${prefix}-${baseId}`;
@@ -481,6 +494,21 @@ function useAuthStyles(theme) {
481
494
  });
482
495
  }, [theme]);
483
496
  }
497
+ function useLoginPrefill({
498
+ initialUsername,
499
+ initialPassword,
500
+ prefillNonce
501
+ }) {
502
+ const [username, setUsername] = react.useState(initialUsername ?? "");
503
+ const [password, setPassword] = react.useState(initialPassword ?? "");
504
+ const [appliedNonce, setAppliedNonce] = react.useState(prefillNonce);
505
+ if (prefillNonce !== appliedNonce) {
506
+ setAppliedNonce(prefillNonce);
507
+ setUsername(initialUsername ?? "");
508
+ setPassword(initialPassword ?? "");
509
+ }
510
+ return { username, password, setUsername, setPassword };
511
+ }
484
512
  var BffAuthStatus = /* @__PURE__ */ ((BffAuthStatus2) => {
485
513
  BffAuthStatus2["Loading"] = "loading";
486
514
  BffAuthStatus2["Authenticated"] = "authenticated";
@@ -592,7 +620,10 @@ function LoginForm({
592
620
  onForgotPassword,
593
621
  onSignUp,
594
622
  testIdPrefix,
595
- chromeless = false
623
+ chromeless = false,
624
+ initialUsername,
625
+ initialPassword,
626
+ prefillNonce
596
627
  }) {
597
628
  const theme = useAuthTheme(themeProp);
598
629
  const styles3 = useAuthStyles(theme);
@@ -601,8 +632,11 @@ function LoginForm({
601
632
  [labelsProp]
602
633
  );
603
634
  const { login, isSubmitting } = useBffAuth({ client, probeOnMount: false });
604
- const [username, setUsername] = react.useState("");
605
- const [password, setPassword] = react.useState("");
635
+ const { username, password, setUsername, setPassword } = useLoginPrefill({
636
+ initialUsername,
637
+ initialPassword,
638
+ prefillNonce
639
+ });
606
640
  const [errorText, setErrorText] = react.useState(null);
607
641
  const runLogin = react.useCallback(async () => {
608
642
  setErrorText(null);
@@ -3132,6 +3166,156 @@ function PreferredMethodHint({
3132
3166
  }
3133
3167
  return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: withTestIdPrefix(AuthTestIds.preferredMethodHint, testIdPrefix), children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.helperText, children: text }) });
3134
3168
  }
3169
+ var PANEL_BORDER_WIDTH = 1;
3170
+ var ACCENT_RAIL_WIDTH = 3;
3171
+ var TITLE_LETTER_SPACING = 0.8;
3172
+ var DESCRIPTION_LINE_HEIGHT_RATIO = 1.45;
3173
+ var HALF = 2;
3174
+ var MONOSPACE_FONT = reactNative.Platform.select({
3175
+ ios: "Menlo",
3176
+ android: "monospace",
3177
+ default: "monospace"
3178
+ });
3179
+ function useDemoCredentialsStyles(theme) {
3180
+ return react.useMemo(() => {
3181
+ const { colors, radii, spacing, typography } = theme;
3182
+ return reactNative.StyleSheet.create({
3183
+ panel: {
3184
+ borderWidth: PANEL_BORDER_WIDTH,
3185
+ borderLeftWidth: ACCENT_RAIL_WIDTH,
3186
+ borderRadius: radii.card,
3187
+ borderColor: colors.border,
3188
+ borderLeftColor: colors.primary,
3189
+ padding: spacing.md,
3190
+ marginBottom: spacing.md,
3191
+ backgroundColor: colors.background
3192
+ },
3193
+ title: {
3194
+ fontSize: typography.caption,
3195
+ fontWeight: "700",
3196
+ letterSpacing: TITLE_LETTER_SPACING,
3197
+ textTransform: "uppercase",
3198
+ color: colors.primary
3199
+ },
3200
+ description: {
3201
+ fontSize: typography.caption,
3202
+ lineHeight: typography.caption * DESCRIPTION_LINE_HEIGHT_RATIO,
3203
+ marginTop: spacing.xs / HALF,
3204
+ marginBottom: spacing.sm,
3205
+ color: colors.textSecondary
3206
+ },
3207
+ row: {
3208
+ flexDirection: "row",
3209
+ alignItems: "center",
3210
+ justifyContent: "space-between",
3211
+ marginTop: spacing.xs / HALF
3212
+ },
3213
+ rowLabel: {
3214
+ fontSize: typography.caption,
3215
+ color: colors.textSecondary
3216
+ },
3217
+ rowValue: {
3218
+ fontFamily: MONOSPACE_FONT,
3219
+ fontSize: typography.caption,
3220
+ fontWeight: "600",
3221
+ borderWidth: PANEL_BORDER_WIDTH,
3222
+ borderRadius: radii.input,
3223
+ borderColor: colors.border,
3224
+ paddingVertical: spacing.xs / HALF,
3225
+ paddingHorizontal: spacing.xs,
3226
+ // Never let a long password squeeze the label off the row.
3227
+ flexShrink: 1,
3228
+ backgroundColor: colors.surface,
3229
+ color: colors.text
3230
+ }
3231
+ });
3232
+ }, [theme]);
3233
+ }
3234
+
3235
+ // src/demo/resolveDemoCredentials.ts
3236
+ function resolveDemoCredentials(credentials) {
3237
+ if (credentials === void 0 || credentials === null) {
3238
+ return null;
3239
+ }
3240
+ const username = typeof credentials.username === "string" ? credentials.username.trim() : "";
3241
+ const password = typeof credentials.password === "string" ? credentials.password : "";
3242
+ const isUnusable = username.length === 0 || password.length === 0;
3243
+ if (isUnusable) {
3244
+ return null;
3245
+ }
3246
+ return { username, password };
3247
+ }
3248
+ function DemoCredentialsHint({
3249
+ credentials,
3250
+ theme: themeProp,
3251
+ labels: labelsProp,
3252
+ testIdPrefix,
3253
+ onUse
3254
+ }) {
3255
+ const theme = useAuthTheme(themeProp);
3256
+ const authStyles = useAuthStyles(theme);
3257
+ const styles3 = useDemoCredentialsStyles(theme);
3258
+ const labels = react.useMemo(
3259
+ () => ({ ...DEFAULT_DEMO_CREDENTIALS_HINT_LABELS, ...labelsProp }),
3260
+ [labelsProp]
3261
+ );
3262
+ const resolved = react.useMemo(() => resolveDemoCredentials(credentials), [credentials]);
3263
+ const handleUse = react.useCallback(() => {
3264
+ if (resolved !== null && onUse !== void 0) {
3265
+ onUse(resolved);
3266
+ }
3267
+ }, [resolved, onUse]);
3268
+ if (resolved === null) {
3269
+ return null;
3270
+ }
3271
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3272
+ reactNative.View,
3273
+ {
3274
+ style: styles3.panel,
3275
+ testID: withTestIdPrefix(AuthTestIds.demoCredentialsHint, testIdPrefix),
3276
+ children: [
3277
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.title, children: labels.title }),
3278
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.description, children: labels.description }),
3279
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.row, children: [
3280
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.rowLabel, children: labels.usernameLabel }),
3281
+ /* @__PURE__ */ jsxRuntime.jsx(
3282
+ reactNative.Text,
3283
+ {
3284
+ selectable: true,
3285
+ style: styles3.rowValue,
3286
+ testID: withTestIdPrefix(AuthTestIds.demoCredentialsUsername, testIdPrefix),
3287
+ children: resolved.username
3288
+ }
3289
+ )
3290
+ ] }),
3291
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.row, children: [
3292
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.rowLabel, children: labels.passwordLabel }),
3293
+ /* @__PURE__ */ jsxRuntime.jsx(
3294
+ reactNative.Text,
3295
+ {
3296
+ selectable: true,
3297
+ style: styles3.rowValue,
3298
+ testID: withTestIdPrefix(AuthTestIds.demoCredentialsPassword, testIdPrefix),
3299
+ children: resolved.password
3300
+ }
3301
+ )
3302
+ ] }),
3303
+ onUse !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
3304
+ reactNative.TouchableOpacity,
3305
+ {
3306
+ accessibilityHint: labels.useCredentialsHint,
3307
+ accessibilityLabel: labels.useCredentials,
3308
+ accessibilityRole: "button",
3309
+ style: authStyles.secondaryButton,
3310
+ testID: withTestIdPrefix(AuthTestIds.demoCredentialsUseButton, testIdPrefix),
3311
+ onPress: handleUse,
3312
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: authStyles.secondaryButtonText, children: labels.useCredentials })
3313
+ }
3314
+ ) : null
3315
+ ]
3316
+ }
3317
+ );
3318
+ }
3135
3319
  var EMPTY_CONFIG = {
3136
3320
  methods: [],
3137
3321
  registrationEnabled: false,
@@ -3237,6 +3421,34 @@ async function syncPreferredMethodFromServer(client) {
3237
3421
  return null;
3238
3422
  }
3239
3423
  }
3424
+ function useDemoCredentials({
3425
+ credentials,
3426
+ prefillOnMount = false
3427
+ } = {}) {
3428
+ const resolved = react.useMemo(() => resolveDemoCredentials(credentials), [credentials]);
3429
+ const [filled, setFilled] = react.useState(
3430
+ prefillOnMount ? resolved : null
3431
+ );
3432
+ const [prefillNonce, setPrefillNonce] = react.useState(0);
3433
+ const handleUse = react.useCallback((next) => {
3434
+ setFilled(next);
3435
+ setPrefillNonce((current) => current + 1);
3436
+ }, []);
3437
+ return react.useMemo(
3438
+ () => ({
3439
+ loginFormProps: {
3440
+ initialUsername: filled?.username,
3441
+ initialPassword: filled?.password,
3442
+ prefillNonce
3443
+ },
3444
+ // `credentials` is passed through unresolved on purpose: the hint applies
3445
+ // `resolveDemoCredentials` itself, so it fails closed even when it is used
3446
+ // standalone, without this hook.
3447
+ hintProps: { credentials, onUse: handleUse }
3448
+ }),
3449
+ [filled, prefillNonce, credentials, handleUse]
3450
+ );
3451
+ }
3240
3452
  async function lazyFetchHttpClient(request) {
3241
3453
  const fetchImpl = typeof fetch === "function" ? fetch.bind(globalThis) : void 0;
3242
3454
  if (fetchImpl === void 0) {
@@ -3286,6 +3498,43 @@ function resolveAccessibleRoutes(user, table) {
3286
3498
  return table.routes.filter((entry) => userRoles.has(entry.role));
3287
3499
  }
3288
3500
 
3501
+ // src/session/performBffLogout.ts
3502
+ function toError4(caught) {
3503
+ return caught instanceof Error ? caught : new Error(String(caught));
3504
+ }
3505
+ function runGuarded(callback, onError) {
3506
+ if (callback === void 0) {
3507
+ return;
3508
+ }
3509
+ try {
3510
+ callback();
3511
+ } catch (caught) {
3512
+ onError?.(toError4(caught));
3513
+ }
3514
+ }
3515
+ function assignLocation(url) {
3516
+ if (typeof window === "undefined") {
3517
+ return;
3518
+ }
3519
+ window.location.assign(url);
3520
+ }
3521
+ async function performBffLogout(options) {
3522
+ const { client, onClearSession, onRedirect, onError, navigateToIdp = assignLocation } = options;
3523
+ runGuarded(onClearSession, onError);
3524
+ let idpLogoutUrl = null;
3525
+ try {
3526
+ idpLogoutUrl = await client.logout({ redirect: false });
3527
+ } catch (caught) {
3528
+ onError?.(toError4(caught));
3529
+ }
3530
+ if (idpLogoutUrl !== null) {
3531
+ const url = idpLogoutUrl;
3532
+ runGuarded(() => navigateToIdp(url), onError);
3533
+ return;
3534
+ }
3535
+ runGuarded(onRedirect, onError);
3536
+ }
3537
+
3289
3538
  Object.defineProperty(exports, "BffAuthClient", {
3290
3539
  enumerable: true,
3291
3540
  get: function () { return authClient.BffAuthClient; }
@@ -3299,6 +3548,7 @@ exports.AuthTestIds = AuthTestIds;
3299
3548
  exports.AuthThemeProvider = AuthThemeProvider;
3300
3549
  exports.BffAuthStatus = BffAuthStatus;
3301
3550
  exports.DEFAULT_AUTH_CARD_MAX_WIDTH = DEFAULT_AUTH_CARD_MAX_WIDTH;
3551
+ exports.DEFAULT_DEMO_CREDENTIALS_HINT_LABELS = DEFAULT_DEMO_CREDENTIALS_HINT_LABELS;
3302
3552
  exports.DEFAULT_DEVICE_PIN_ENROLL_LABELS = DEFAULT_DEVICE_PIN_ENROLL_LABELS;
3303
3553
  exports.DEFAULT_DEVICE_PIN_SETTINGS_LABELS = DEFAULT_DEVICE_PIN_SETTINGS_LABELS;
3304
3554
  exports.DEFAULT_DEVICE_PIN_UNLOCK_LABELS = DEFAULT_DEVICE_PIN_UNLOCK_LABELS;
@@ -3314,6 +3564,7 @@ exports.DEFAULT_PREFERRED_METHOD_SETTINGS_LABELS = DEFAULT_PREFERRED_METHOD_SETT
3314
3564
  exports.DEFAULT_RESET_PASSWORD_LABELS = DEFAULT_RESET_PASSWORD_LABELS;
3315
3565
  exports.DEVICE_PIN_ALLOWED_DIGITS = DEVICE_PIN_ALLOWED_DIGITS;
3316
3566
  exports.DEVICE_PIN_DEFAULT_DIGITS = DEVICE_PIN_DEFAULT_DIGITS;
3567
+ exports.DemoCredentialsHint = DemoCredentialsHint;
3317
3568
  exports.DevicePinEnrollErrorKey = DevicePinEnrollErrorKey;
3318
3569
  exports.DevicePinEnrollForm = DevicePinEnrollForm;
3319
3570
  exports.DevicePinErrorKey = DevicePinErrorKey;
@@ -3348,10 +3599,12 @@ exports.defaultAuthTheme = defaultAuthTheme;
3348
3599
  exports.isAllowedPinDigits = isAllowedPinDigits;
3349
3600
  exports.isPasswordValid = isPasswordValid;
3350
3601
  exports.isValidForgotPasswordEmail = isValidForgotPasswordEmail;
3602
+ exports.performBffLogout = performBffLogout;
3351
3603
  exports.readCachedPreferredMethod = readCachedPreferredMethod;
3352
3604
  exports.readPasskeyError = readPasskeyError;
3353
3605
  exports.readPasskeyRegistered = readPasskeyRegistered;
3354
3606
  exports.resolveAccessibleRoutes = resolveAccessibleRoutes;
3607
+ exports.resolveDemoCredentials = resolveDemoCredentials;
3355
3608
  exports.resolvePostLoginRoute = resolvePostLoginRoute;
3356
3609
  exports.startPasskeyLogin = startPasskeyLogin;
3357
3610
  exports.startPasskeyRegistration = startPasskeyRegistration;
@@ -3361,6 +3614,7 @@ exports.useBffAuth = useBffAuth;
3361
3614
  exports.useBffForgotPassword = useBffForgotPassword;
3362
3615
  exports.useBffLoginConfig = useBffLoginConfig;
3363
3616
  exports.useBffResetPassword = useBffResetPassword;
3617
+ exports.useDemoCredentials = useDemoCredentials;
3364
3618
  exports.useDevicePinDisable = useDevicePinDisable;
3365
3619
  exports.useDevicePinEnroll = useDevicePinEnroll;
3366
3620
  exports.useDevicePinUnlock = useDevicePinUnlock;