@object-ui/auth 17.6.0 → 18.0.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +598 -0
  2. package/README.md +124 -19
  3. package/dist/ActiveOrganizationStorage.d.ts +32 -1
  4. package/dist/ActiveOrganizationStorage.d.ts.map +1 -1
  5. package/dist/ActiveOrganizationStorage.js +72 -3
  6. package/dist/AuthContext.d.ts +10 -3
  7. package/dist/AuthContext.d.ts.map +1 -1
  8. package/dist/AuthProvider.d.ts.map +1 -1
  9. package/dist/AuthProvider.js +101 -18
  10. package/dist/LoginForm.d.ts +35 -4
  11. package/dist/LoginForm.d.ts.map +1 -1
  12. package/dist/LoginForm.js +16 -4
  13. package/dist/RegisterForm.d.ts +21 -2
  14. package/dist/RegisterForm.d.ts.map +1 -1
  15. package/dist/RegisterForm.js +3 -4
  16. package/dist/SocialSignInButtons.d.ts +15 -2
  17. package/dist/SocialSignInButtons.d.ts.map +1 -1
  18. package/dist/SocialSignInButtons.js +3 -3
  19. package/dist/UserMenu.d.ts +1 -1
  20. package/dist/UserMenu.js +1 -1
  21. package/dist/auth-gate-events.d.ts +6 -4
  22. package/dist/auth-gate-events.d.ts.map +1 -1
  23. package/dist/auth-gate-events.js +11 -1
  24. package/dist/authStyles.d.ts +8 -3
  25. package/dist/authStyles.d.ts.map +1 -1
  26. package/dist/authStyles.js +8 -3
  27. package/dist/createAuthClient.d.ts +15 -1
  28. package/dist/createAuthClient.d.ts.map +1 -1
  29. package/dist/createAuthClient.js +54 -1
  30. package/dist/createAuthenticatedFetch.d.ts +5 -4
  31. package/dist/createAuthenticatedFetch.d.ts.map +1 -1
  32. package/dist/createAuthenticatedFetch.js +3 -3
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +6 -0
  36. package/dist/types.d.ts +53 -2
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/useWorkspaceAdminStatus.d.ts +76 -27
  39. package/dist/useWorkspaceAdminStatus.d.ts.map +1 -1
  40. package/dist/useWorkspaceAdminStatus.js +146 -40
  41. package/package.json +6 -7
@@ -9,7 +9,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
9
9
  import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
10
10
  import { authGateEvents } from './auth-gate-events.js';
11
11
  import { AuthCtx } from './AuthContext.js';
12
- import { createAuthClient, TokenStorage } from './createAuthClient.js';
12
+ import { createAuthClient, takePreflightSession, TokenStorage } from './createAuthClient.js';
13
13
  import { ActiveOrganizationStorage, SessionUserScope } from './ActiveOrganizationStorage.js';
14
14
  /**
15
15
  * Prefix of every `MetadataProvider` seed entry in `sessionStorage`
@@ -29,8 +29,8 @@ const METADATA_SEED_CACHE_PREFIX = 'objectui:metadata:';
29
29
  * (objectui#5198).
30
30
  *
31
31
  * `sessionStorage` is per-TAB, not per-session, and no sign-out call site
32
- * reloads the page — `AppSidebar`, `AppHeader`, `UserMenu` and
33
- * `RemediationOverlay` all just call `signOut()` and let the SPA keep running.
32
+ * reloads the page — `AppHeader`, `UserMenu` and `RemediationOverlay` all
33
+ * just call `signOut()` and let the SPA keep running.
34
34
  * So without this the next person to sign in in the same tab (a shared or
35
35
  * kiosk browser, a handover, a support session) is SEEDED from the previous
36
36
  * user's entry, and what is in it is that user's PERMISSION-FILTERED app list —
@@ -141,9 +141,20 @@ function purgeSignedOutClientCaches() {
141
141
  */
142
142
  export function AuthProvider({ authUrl, client: externalClient, onAuthStateChange, enabled = true, previewMode, children, }) {
143
143
  const client = useMemo(() => externalClient ?? createAuthClient({ baseURL: authUrl }), [externalClient, authUrl]);
144
- const [user, setUser] = useState(null);
145
- const [session, setSession] = useState(null);
146
- const [isLoading, setIsLoading] = useState(true);
144
+ // objectui#12808 — the console's pre-React preflight already asked
145
+ // `get-session` with the stored Bearer. When that answer is still the one we
146
+ // would get, it IS the first state, and the mount below does not ask again.
147
+ // Taken once per page load, in an initializer so it runs once per instance
148
+ // (StrictMode keeps the first call's result). Only for a provider that builds
149
+ // its own client from `authUrl`: an injected client may talk to another server.
150
+ const [bootSession] = useState(() => (enabled && previewMode == null && !externalClient ? takePreflightSession(authUrl) : null));
151
+ const [user, setUser] = useState(() => bootSession?.user ?? null);
152
+ const [session, setSession] = useState(() => bootSession?.session ?? null);
153
+ const [isLoading, setIsLoading] = useState(() => bootSession === null);
154
+ // What the boot answer was taken for. The mount effect skips its load while
155
+ // it still holds (a StrictMode effect replay included); the first run that
156
+ // finds anything else drops it for good, so every later run loads as before.
157
+ const bootSessionFor = useRef(bootSession ? authUrl : null);
147
158
  const [error, setError] = useState(null);
148
159
  const [remediationRequired, setRemediationRequired] = useState(null);
149
160
  // Organization / workspace state
@@ -181,6 +192,16 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
181
192
  // stale, previous-organization data over the newer one (see
182
193
  // `switchOrganization`).
183
194
  const sessionLoadGeneration = useRef(0);
195
+ // objectui#12809 — the member row asked for at the same time as
196
+ // `organization/list`, when the session already names its active
197
+ // organization. `get-active-member` answers for the session's active
198
+ // organization, so it does not have to wait for the list. Held with the org
199
+ // and user it was asked for and taken once, by the first
200
+ // `refreshActiveMember` for that same organization; anything else drops it
201
+ // (a switch, a sign-out, an active organization the list did not confirm).
202
+ // Not a cache: the answer is used for the moment it was asked in, never for
203
+ // a later question.
204
+ const bootMemberRef = useRef(null);
184
205
  /**
185
206
  * The ONE session loader. Runs on mount (below) and on every later
186
207
  * re-resolution — `refreshSession` on the context, and the rotation
@@ -260,6 +281,9 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
260
281
  }, [enabled, isPreviewMode, loadSession]);
261
282
  // Load session on mount (only if auth is enabled and not in preview mode)
262
283
  useEffect(() => {
284
+ if (isPreviewMode || !enabled || externalClient || bootSessionFor.current !== authUrl) {
285
+ bootSessionFor.current = null;
286
+ }
263
287
  if (isPreviewMode) {
264
288
  // Preview mode: simulate a user based on previewMode config
265
289
  const role = previewMode.simulatedRole ?? 'admin';
@@ -303,10 +327,13 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
303
327
  setIsLoading(false);
304
328
  return;
305
329
  }
330
+ // The first state already holds the preflight's answer (see `bootSession`).
331
+ if (bootSessionFor.current !== null)
332
+ return;
306
333
  let cancelled = false;
307
334
  loadSession(() => cancelled);
308
335
  return () => { cancelled = true; };
309
- }, [client, enabled, isPreviewMode, previewMode, loadSession]);
336
+ }, [client, enabled, isPreviewMode, previewMode, loadSession, authUrl, externalClient]);
310
337
  // Notify on auth state changes
311
338
  useEffect(() => {
312
339
  onAuthStateChange?.({
@@ -334,11 +361,11 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
334
361
  setIsLoading(false);
335
362
  }
336
363
  }, [client]);
337
- const signUp = useCallback(async (name, email, password) => {
364
+ const signUp = useCallback(async (name, email, password, callbackURL) => {
338
365
  setIsLoading(true);
339
366
  setError(null);
340
367
  try {
341
- const result = await client.signUp({ name, email, password });
368
+ const result = await client.signUp({ name, email, password, ...(callbackURL ? { callbackURL } : {}) });
342
369
  if (result.requiresVerification) {
343
370
  // No session was created — the user must verify their email before
344
371
  // they can sign in. Leave auth state untouched so the caller can
@@ -377,6 +404,7 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
377
404
  // this file: drop the in-memory reference and the stored id together.
378
405
  setOrganizations([]);
379
406
  setActiveOrganization(null);
407
+ bootMemberRef.current = null;
380
408
  }
381
409
  catch (err) {
382
410
  setError(err instanceof Error ? err : new Error(String(err)));
@@ -547,7 +575,17 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
547
575
  }
548
576
  }, [client]);
549
577
  // --- Organization methods ---
550
- const refreshOrganizations = useCallback(async (isCancelled) => {
578
+ /**
579
+ * `sessionActiveOrgId` — objectui#12809. Passed only by the load that follows
580
+ * a `get-session` answer (the effect below), and only when that answer
581
+ * carries the organization plugin's `activeOrganizationId`. The session
582
+ * already names the server's active organization, so it is named from that
583
+ * id and the list answer: no `get-full-organization` round trip, whose extra
584
+ * fields (members, invitations) nothing the console reads. Every other caller
585
+ * (a mutation, `refreshOrganizations()` on the context) asks the server, as
586
+ * before: the session in hand may predate what that mutation changed.
587
+ */
588
+ const refreshOrganizations = useCallback(async (isCancelled, sessionActiveOrgId) => {
551
589
  if (!enabled || isPreviewMode)
552
590
  return;
553
591
  setIsOrganizationsLoading(true);
@@ -559,12 +597,19 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
559
597
  // If no active org is set but orgs exist, try to get active from server
560
598
  if (orgs.length > 0 && !activeOrganization) {
561
599
  let active = null;
562
- try {
563
- active = await client.getActiveOrganization();
600
+ if (sessionActiveOrgId !== undefined) {
601
+ // An id the list does not carry is no membership of this user's: the
602
+ // server's full-organization read refuses it the same way.
603
+ active = (sessionActiveOrgId && orgs.find((o) => o.id === sessionActiveOrgId)) || null;
564
604
  }
565
- catch {
566
- // No active org set — that's fine
567
- active = null;
605
+ else {
606
+ try {
607
+ active = await client.getActiveOrganization();
608
+ }
609
+ catch {
610
+ // No active org set — that's fine
611
+ active = null;
612
+ }
568
613
  }
569
614
  // Single-membership repair (ADR-0081): a session created before the
570
615
  // server-side active-org stamp existed carries no active org even
@@ -579,10 +624,27 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
579
624
  active = null;
580
625
  }
581
626
  }
582
- if (active && !isCancelled?.()) {
627
+ if (isCancelled?.())
628
+ return orgs;
629
+ if (bootMemberRef.current && bootMemberRef.current.orgId !== active?.id) {
630
+ bootMemberRef.current = null;
631
+ }
632
+ if (active) {
583
633
  setActiveOrganization(active);
584
634
  ActiveOrganizationStorage.set(active.id);
585
635
  }
636
+ else if (activeOrgIdRef.current === null) {
637
+ // objectui#12698 — the server answered "no active organization"
638
+ // (and, at one membership, refused the repair above). The stored id
639
+ // is a cache of a SERVER-owned fact, so it goes with that answer:
640
+ // left in place it keeps stamping `X-Tenant-ID` and keying the
641
+ // metadata seed cache for an organization this session does not
642
+ // hold — e.g. the one a refused `set-active` cleared server-side
643
+ // (better-auth nulls the session's active org before it refuses a
644
+ // non-member). The `activeOrgIdRef` guard keeps a user switch that
645
+ // landed while this request was in flight from being wiped.
646
+ ActiveOrganizationStorage.clear();
647
+ }
586
648
  }
587
649
  return orgs;
588
650
  }
@@ -634,8 +696,12 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
634
696
  // A switch re-opens the question: until `getActiveMember()` answers for the
635
697
  // NEW org, `activeMember` still holds the OLD org's row.
636
698
  setActiveMemberResolved(false);
699
+ const boot = bootMemberRef.current;
700
+ bootMemberRef.current = null;
637
701
  try {
638
- const member = await client.getActiveMember();
702
+ const member = boot && boot.orgId === activeOrganization.id
703
+ ? await boot.answer
704
+ : await client.getActiveMember();
639
705
  setActiveMember(member);
640
706
  }
641
707
  catch (err) {
@@ -682,11 +748,27 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
682
748
  refreshActiveMember();
683
749
  }, [refreshActiveMember]);
684
750
  // Load organizations once user is authenticated
751
+ //
752
+ // objectui#12809 — a session answer that carries the organization plugin's
753
+ // `activeOrganizationId` already names the server's active organization, so
754
+ // the member row is asked for now, beside `organization/list`, instead of
755
+ // after the active organization lands. A StrictMode effect replay finds the
756
+ // same question already asked and does not ask again.
685
757
  useEffect(() => {
686
758
  if (!(user && enabled && !isPreviewMode))
687
759
  return;
688
760
  let cancelled = false;
689
- refreshOrganizations(() => cancelled);
761
+ const sessionActiveOrgId = session?.activeOrganizationId;
762
+ if (typeof sessionActiveOrgId === 'string' && !activeOrganization) {
763
+ const asked = bootMemberRef.current;
764
+ if (!asked || asked.orgId !== sessionActiveOrgId || asked.userId !== user.id) {
765
+ const answer = client.getActiveMember();
766
+ // Taken later, or dropped: either way it must not surface as unhandled.
767
+ answer.catch(() => undefined);
768
+ bootMemberRef.current = { orgId: sessionActiveOrgId, userId: user.id, answer };
769
+ }
770
+ }
771
+ refreshOrganizations(() => cancelled, sessionActiveOrgId);
690
772
  return () => { cancelled = true; };
691
773
  }, [user, enabled, isPreviewMode]); // eslint-disable-line react-hooks/exhaustive-deps
692
774
  // objectui#5750 — keep `activeOrgIdRef` current for every path that can
@@ -728,6 +810,7 @@ export function AuthProvider({ authUrl, client: externalClient, onAuthStateChang
728
810
  const switchOrganization = useCallback(async (orgId) => {
729
811
  setError(null);
730
812
  const previousOrgId = activeOrgIdRef.current;
813
+ bootMemberRef.current = null;
731
814
  sessionLoadInFlight.current = true;
732
815
  try {
733
816
  const org = await client.setActiveOrganization(orgId);
@@ -22,8 +22,18 @@ export interface LoginFormLabels {
22
22
  submittingButton?: string;
23
23
  noAccountText?: string;
24
24
  signUpText?: string;
25
- /** Divider label between social sign-in and email/password (defaults to "or") */
25
+ /**
26
+ * Divider label between the social provider buttons and the email/password
27
+ * form (defaults to "or continue with email"). Rendered only when the server
28
+ * reports at least one social provider, and only while that form is shown:
29
+ * under SSO-only mode it appears once "Use a password instead" reveals it.
30
+ */
26
31
  orText?: string;
32
+ /**
33
+ * Social provider button label (defaults to "Continue with {provider}");
34
+ * `{provider}` is replaced with the provider's display name.
35
+ */
36
+ socialButton?: string;
27
37
  /** Label for the SSO sign-in button (defaults to "Sign in with SSO") */
28
38
  ssoButton?: string;
29
39
  /**
@@ -52,19 +62,32 @@ export interface LoginFormLabels {
52
62
  usePasswordSignInText?: string;
53
63
  /** Description shown above the form in SSO-only mode (defaults to "Sign in with your organization's single sign-on"). */
54
64
  ssoOnlyDescription?: string;
65
+ /** Accessible name of the password field's reveal toggle while the password is hidden (defaults to "Show password"). */
66
+ showPasswordText?: string;
67
+ /** Accessible name of the password field's reveal toggle while the password is shown (defaults to "Hide password"). */
68
+ hidePasswordText?: string;
55
69
  }
56
70
  export interface LoginFormProps {
57
71
  /** Callback on successful login */
58
72
  onSuccess?: () => void;
59
73
  /** Callback on login error */
60
74
  onError?: (error: Error) => void;
61
- /** Link to registration page */
75
+ /**
76
+ * Link to registration page. The "Sign up" link renders only when this is
77
+ * set: there is no default, so leaving it out (or passing `undefined`, as a
78
+ * caller does when the server reports `emailPassword.disableSignUp`) renders
79
+ * no link.
80
+ */
62
81
  registerUrl?: string;
63
82
  /** Link to forgot password page */
64
83
  forgotPasswordUrl?: string;
65
84
  /** Custom title */
66
85
  title?: string;
67
- /** Custom description */
86
+ /**
87
+ * Custom description. Not shown while the server's auth config is still
88
+ * resolving, since it may describe a password form the server does not
89
+ * offer; under SSO-only mode `labels.ssoOnlyDescription` replaces it.
90
+ */
68
91
  description?: string;
69
92
  /** Custom icon shown above the title (defaults to a small lock disc) */
70
93
  icon?: React.ReactNode;
@@ -81,6 +104,14 @@ export interface LoginFormProps {
81
104
  errorMessages?: Record<string, string>;
82
105
  /** Hide the icon disc above the form title. Defaults to false. */
83
106
  hideIcon?: boolean;
107
+ /**
108
+ * Render no header — no icon disc, no title, no description — because the
109
+ * host draws its own above the form. Defaults to false, which renders the
110
+ * header exactly as before. Under SSO-only ("enforced") mode the form still
111
+ * says how to sign in: the SSO-only description, which the header would
112
+ * otherwise carry, renders as one muted line above the federated buttons.
113
+ */
114
+ hideHeader?: boolean;
84
115
  }
85
116
  /**
86
117
  * Login form component with email/password authentication.
@@ -96,5 +127,5 @@ export interface LoginFormProps {
96
127
  * />
97
128
  * ```
98
129
  */
99
- export declare function LoginForm({ onSuccess, onError, registerUrl, forgotPasswordUrl, title, description, icon, hideIcon, linkComponent: LinkComp, labels, errorMessages, }: LoginFormProps): React.JSX.Element;
130
+ export declare function LoginForm({ onSuccess, onError, registerUrl, forgotPasswordUrl, title, description, icon, hideIcon, hideHeader, linkComponent: LinkComp, labels, errorMessages, }: LoginFormProps): React.JSX.Element;
100
131
  //# sourceMappingURL=LoginForm.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LoginForm.d.ts","sourceRoot":"","sources":["../src/LoginForm.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAYzD,4CAA4C;AAC5C,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8GAA8G;IAC9G,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gGAAgG;IAChG,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,8FAA8F;IAC9F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iGAAiG;IACjG,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,yHAAyH;IACzH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,gCAAgC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mCAAmC;IACnC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mBAAmB;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;IAC5D,uCAAuC;IACvC,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAsBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,EACxB,SAAS,EACT,OAAO,EACP,WAAyB,EACzB,iBAAsC,EACtC,KAAiC,EACjC,WAAyD,EACzD,IAAI,EACJ,QAAgB,EAChB,aAAa,EAAE,QAAsB,EACrC,MAAW,EACX,aAAa,GACd,EAAE,cAAc,qBAgbhB"}
1
+ {"version":3,"file":"LoginForm.d.ts","sourceRoot":"","sources":["../src/LoginForm.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAWzD,4CAA4C;AAC5C,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8GAA8G;IAC9G,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gGAAgG;IAChG,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,8FAA8F;IAC9F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iGAAiG;IACjG,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,yHAAyH;IACzH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wHAAwH;IACxH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,uHAAuH;IACvH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mCAAmC;IACnC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mBAAmB;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;IAC5D,uCAAuC;IACvC,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAwDD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,EACxB,SAAS,EACT,OAAO,EACP,WAAW,EACX,iBAAsC,EACtC,KAAiC,EACjC,WAAyD,EACzD,IAAI,EACJ,QAAgB,EAChB,UAAkB,EAClB,aAAa,EAAE,QAAsB,EACrC,MAAW,EACX,aAAa,GACd,EAAE,cAAc,qBAkehB"}
package/dist/LoginForm.js CHANGED
@@ -12,6 +12,8 @@ import { SocialSignInButtons } from './SocialSignInButtons.js';
12
12
  import { looksLikePhoneIdentifier, normalizePhoneIdentifier } from './phone-identifier.js';
13
13
  import { AUTH_FIELD_LABEL_CLASS, AUTH_INPUT_CLASS, AUTH_LINK_CLASS, AUTH_PRIMARY_BUTTON_CLASS, AuthErrorBanner, AuthFormHeader, AuthSpinner, } from './authStyles.js';
14
14
  const DefaultLink = ({ href, className, children }) => (_jsx("a", { href: href, className: className, children: children }));
15
+ const EyeIcon = () => (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "h-4 w-4", "aria-hidden": "true", children: [_jsx("path", { d: "M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" }), _jsx("circle", { cx: "12", cy: "12", r: "3" })] }));
16
+ const EyeOffIcon = () => (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "h-4 w-4", "aria-hidden": "true", children: [_jsx("path", { d: "M10.73 5.08A10.4 10.4 0 0 1 12 5c7 0 10 7 10 7a13.2 13.2 0 0 1-1.67 2.68" }), _jsx("path", { d: "M6.61 6.61A13.5 13.5 0 0 0 2 12s3 7 10 7a9.7 9.7 0 0 0 5.39-1.61" }), _jsx("path", { d: "M9.88 9.88a3 3 0 1 0 4.24 4.24" }), _jsx("path", { d: "m2 2 20 20" })] }));
15
17
  const DefaultLockIcon = () => (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", className: "h-6 w-6", "aria-hidden": "true", children: [_jsx("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2" }), _jsx("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" })] }));
16
18
  /**
17
19
  * Login form component with email/password authentication.
@@ -27,10 +29,13 @@ const DefaultLockIcon = () => (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none"
27
29
  * />
28
30
  * ```
29
31
  */
30
- export function LoginForm({ onSuccess, onError, registerUrl = '/register', forgotPasswordUrl = '/forgot-password', title = 'Sign in to your account', description = 'Enter your email and password to continue', icon, hideIcon = false, linkComponent: LinkComp = DefaultLink, labels = {}, errorMessages, }) {
32
+ export function LoginForm({ onSuccess, onError, registerUrl, forgotPasswordUrl = '/forgot-password', title = 'Sign in to your account', description = 'Enter your email and password to continue', icon, hideIcon = false, hideHeader = false, linkComponent: LinkComp = DefaultLink, labels = {}, errorMessages, }) {
31
33
  const { signIn, isLoading, getAuthConfig, sendPhoneOtp, signInWithPhoneOtp, signInWithPhonePassword } = useAuth();
32
34
  const [email, setEmail] = useState('');
33
35
  const [password, setPassword] = useState('');
36
+ // The password field's reveal toggle. Local and ephemeral on purpose: a
37
+ // revealed password must not survive a remount or reach any URL or storage.
38
+ const [passwordVisible, setPasswordVisible] = useState(false);
34
39
  const [error, setError] = useState(null);
35
40
  // Phone-OTP sign-in (framework#2780). Only offered when the server reports
36
41
  // `features.phoneNumberOtp` — i.e. the phoneNumber plugin is on AND a
@@ -117,7 +122,6 @@ export function LoginForm({ onSuccess, onError, registerUrl = '/register', forgo
117
122
  submittingButton: labels.submittingButton ?? 'Signing in…',
118
123
  noAccountText: labels.noAccountText ?? "Don't have an account?",
119
124
  signUpText: labels.signUpText ?? 'Sign up',
120
- orText: labels.orText ?? 'or',
121
125
  ssoButton: labels.ssoButton ?? 'Sign in with SSO',
122
126
  usePasswordText: labels.usePasswordText ?? 'Use a password instead',
123
127
  backToSsoText: labels.backToSsoText ?? 'Back to single sign-on',
@@ -130,6 +134,8 @@ export function LoginForm({ onSuccess, onError, registerUrl = '/register', forgo
130
134
  resendOtpCountdownText: labels.resendOtpCountdownText ?? 'Resend in {seconds}s',
131
135
  usePhoneOtpText: labels.usePhoneOtpText ?? 'Sign in with verification code',
132
136
  usePasswordSignInText: labels.usePasswordSignInText ?? 'Sign in with password instead',
137
+ showPasswordText: labels.showPasswordText ?? 'Show password',
138
+ hidePasswordText: labels.hidePasswordText ?? 'Hide password',
133
139
  };
134
140
  // Tick the resend cooldown down once per second while it's armed.
135
141
  useEffect(() => {
@@ -234,10 +240,16 @@ export function LoginForm({ onSuccess, onError, registerUrl = '/register', forgo
234
240
  setSsoSubmitting(false);
235
241
  }
236
242
  };
237
- return (_jsxs("div", { className: "mx-auto flex w-full flex-col justify-center space-y-7 sm:w-[400px]", children: [_jsx(AuthFormHeader, { icon: hideIcon ? undefined : (icon ?? _jsx(DefaultLockIcon, {})), title: title, description: ssoEnforced && !showPasswordFallback ? l.ssoOnlyDescription : description }), _jsx("div", { className: "space-y-5", children: configPending ? (_jsx("div", { className: "flex justify-center py-10", role: "status", "aria-live": "polite", "data-testid": "login-config-loading", children: _jsx(AuthSpinner, {}) })) : (_jsxs(_Fragment, { children: [_jsx(SocialSignInButtons, { mode: "sign-in", onProvidersResolved: (hasProviders) => setHasSocialProviders(hasProviders) }), passwordFormVisible ? (mode === 'phone-otp' ? (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [error && _jsx(AuthErrorBanner, { message: error }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "login-phone", className: AUTH_FIELD_LABEL_CLASS, children: l.phoneLabel }), _jsx("input", { id: "login-phone", type: "tel", placeholder: l.phonePlaceholder, value: phone, onChange: (e) => setPhone(e.target.value), required: true, autoComplete: "tel", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "login-otp", className: AUTH_FIELD_LABEL_CLASS, children: l.otpCodeLabel }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { id: "login-otp", type: "text", inputMode: "numeric", placeholder: l.otpCodePlaceholder, value: otpCode, onChange: (e) => setOtpCode(e.target.value), required: true, autoComplete: "one-time-code", disabled: isLoading, className: AUTH_INPUT_CLASS }), _jsxs("button", { type: "button", onClick: handleSendOtp, disabled: otpSending || otpCooldown > 0 || !phone.trim() || isLoading, className: "shrink-0 rounded-md border border-input bg-background px-3 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50", children: [otpSending && _jsx(AuthSpinner, {}), otpCooldown > 0
243
+ // The SSO-only view: enforced mode with the break-glass form still closed.
244
+ const ssoOnlyView = ssoEnforced && !showPasswordFallback;
245
+ return (_jsxs("div", { className: `mx-auto flex w-full flex-col justify-center sm:max-w-[400px] ${hideHeader ? 'space-y-4' : 'space-y-7'}`, children: [hideHeader ? (ssoOnlyView ? (_jsx("p", { className: "text-[13px] leading-relaxed text-muted-foreground", children: l.ssoOnlyDescription })) : null) : (_jsx(AuthFormHeader, { icon: hideIcon ? undefined : (icon ?? _jsx(DefaultLockIcon, {})), title: title,
246
+ // No description while the config resolves: `ssoEnforced` is not
247
+ // known yet, and the password form's copy would greet an SSO-only
248
+ // deployment (objectui#12682).
249
+ description: configPending ? undefined : ssoOnlyView ? l.ssoOnlyDescription : description })), _jsx("div", { className: "space-y-5", children: configPending ? (_jsx("div", { className: "flex justify-center py-10", role: "status", "aria-live": "polite", "data-testid": "login-config-loading", children: _jsx(AuthSpinner, {}) })) : (_jsxs(_Fragment, { children: [_jsx(SocialSignInButtons, { mode: "sign-in", dividerText: labels.orText, hideDivider: !passwordFormVisible, buttonText: labels.socialButton, onProvidersResolved: (hasProviders) => setHasSocialProviders(hasProviders) }), passwordFormVisible ? (mode === 'phone-otp' ? (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [error && _jsx(AuthErrorBanner, { message: error }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "login-phone", className: AUTH_FIELD_LABEL_CLASS, children: l.phoneLabel }), _jsx("input", { id: "login-phone", type: "tel", placeholder: l.phonePlaceholder, value: phone, onChange: (e) => setPhone(e.target.value), required: true, autoComplete: "tel", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "login-otp", className: AUTH_FIELD_LABEL_CLASS, children: l.otpCodeLabel }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { id: "login-otp", type: "text", inputMode: "numeric", placeholder: l.otpCodePlaceholder, value: otpCode, onChange: (e) => setOtpCode(e.target.value), required: true, autoComplete: "one-time-code", disabled: isLoading, className: AUTH_INPUT_CLASS }), _jsxs("button", { type: "button", onClick: handleSendOtp, disabled: otpSending || otpCooldown > 0 || !phone.trim() || isLoading, className: "shrink-0 rounded-md border border-input bg-background px-3 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50", children: [otpSending && _jsx(AuthSpinner, {}), otpCooldown > 0
238
250
  ? l.resendOtpCountdownText.replace('{seconds}', String(otpCooldown))
239
251
  : l.sendOtpButton] })] })] }), _jsxs("button", { type: "submit", disabled: isLoading || !otpSent, className: AUTH_PRIMARY_BUTTON_CLASS, children: [isLoading && _jsx(AuthSpinner, {}), isLoading ? l.submittingButton : l.submitButton] }), _jsx("button", { type: "button", onClick: () => switchMode('password'), className: "w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.usePasswordSignInText })] })) : (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [error && _jsx(AuthErrorBanner, { message: error }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "login-email", className: AUTH_FIELD_LABEL_CLASS, children: phonePasswordEnabled ? l.emailOrPhoneLabel : l.emailLabel }), _jsx("input", { id: "login-email",
240
252
  // `text` (not `email`) when a phone number is also accepted, so the
241
253
  // browser doesn't reject a phone-shaped value on native validation.
242
- type: phonePasswordEnabled ? 'text' : 'email', inputMode: phonePasswordEnabled ? 'text' : 'email', placeholder: phonePasswordEnabled ? l.emailOrPhonePlaceholder : l.emailPlaceholder, value: email, onChange: (e) => setEmail(e.target.value), required: true, autoComplete: "username", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("label", { htmlFor: "login-password", className: AUTH_FIELD_LABEL_CLASS, children: l.passwordLabel }), forgotPasswordUrl && (_jsx(LinkComp, { href: forgotPasswordUrl, className: "text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.forgotPasswordText }))] }), _jsx("input", { id: "login-password", type: "password", placeholder: l.passwordPlaceholder, value: password, onChange: (e) => setPassword(e.target.value), required: true, autoComplete: "current-password", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("button", { type: "submit", disabled: isLoading, className: AUTH_PRIMARY_BUTTON_CLASS, children: [isLoading && _jsx(AuthSpinner, {}), isLoading ? l.submittingButton : l.submitButton] }), ssoEnabled && (_jsxs("button", { type: "button", onClick: handleSso, disabled: isLoading || ssoSubmitting, "aria-busy": ssoSubmitting, className: "flex w-full items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50", children: [ssoSubmitting && _jsx(AuthSpinner, {}), l.ssoButton] })), phoneOtpEnabled && (_jsx("button", { type: "button", onClick: () => switchMode('phone-otp'), className: "w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.usePhoneOtpText })), ssoEnforced && showPasswordFallback && (_jsx("button", { type: "button", onClick: () => setShowPasswordFallback(false), className: "w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.backToSsoText }))] }))) : (_jsxs("div", { className: "space-y-4", children: [error && _jsx(AuthErrorBanner, { message: error }), _jsx("button", { type: "button", onClick: () => setShowPasswordFallback(true), className: "w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.usePasswordText })] }))] })) }), registerUrl && !ssoEnforced && (_jsxs("p", { className: "px-8 text-center text-sm text-muted-foreground", children: [l.noAccountText, ' ', _jsx(LinkComp, { href: registerUrl, className: AUTH_LINK_CLASS, children: l.signUpText })] }))] }));
254
+ type: phonePasswordEnabled ? 'text' : 'email', inputMode: phonePasswordEnabled ? 'text' : 'email', placeholder: phonePasswordEnabled ? l.emailOrPhonePlaceholder : l.emailPlaceholder, value: email, onChange: (e) => setEmail(e.target.value), required: true, autoComplete: "username", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("label", { htmlFor: "login-password", className: AUTH_FIELD_LABEL_CLASS, children: l.passwordLabel }), forgotPasswordUrl && (_jsx(LinkComp, { href: forgotPasswordUrl, className: "text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.forgotPasswordText }))] }), _jsxs("div", { className: "relative", children: [_jsx("input", { id: "login-password", type: passwordVisible ? 'text' : 'password', placeholder: l.passwordPlaceholder, value: password, onChange: (e) => setPassword(e.target.value), required: true, autoComplete: "current-password", disabled: isLoading, className: `${AUTH_INPUT_CLASS} pr-10` }), _jsx("button", { type: "button", "data-testid": "login-password-toggle", onClick: () => setPasswordVisible((v) => !v), "aria-label": passwordVisible ? l.hidePasswordText : l.showPasswordText, "aria-controls": "login-password", disabled: isLoading, className: "absolute right-1.5 top-1/2 inline-flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:opacity-50", children: passwordVisible ? _jsx(EyeOffIcon, {}) : _jsx(EyeIcon, {}) })] })] }), _jsxs("button", { type: "submit", disabled: isLoading, className: AUTH_PRIMARY_BUTTON_CLASS, children: [isLoading && _jsx(AuthSpinner, {}), isLoading ? l.submittingButton : l.submitButton] }), ssoEnabled && (_jsxs("button", { type: "button", onClick: handleSso, disabled: isLoading || ssoSubmitting, "aria-busy": ssoSubmitting, className: "flex h-10 w-full items-center justify-center gap-2 rounded-lg border border-input bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50", children: [ssoSubmitting && _jsx(AuthSpinner, {}), l.ssoButton] })), phoneOtpEnabled && (_jsx("button", { type: "button", onClick: () => switchMode('phone-otp'), className: "w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.usePhoneOtpText })), ssoEnforced && showPasswordFallback && (_jsx("button", { type: "button", onClick: () => setShowPasswordFallback(false), className: "w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.backToSsoText }))] }))) : (_jsxs("div", { className: "space-y-4", children: [error && _jsx(AuthErrorBanner, { message: error }), _jsx("button", { type: "button", onClick: () => setShowPasswordFallback(true), className: "w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline", children: l.usePasswordText })] }))] })) }), registerUrl && !ssoEnforced && (_jsxs("p", { className: "px-8 text-center text-[13px] text-muted-foreground", children: [l.noAccountText, ' ', _jsx(LinkComp, { href: registerUrl, className: AUTH_LINK_CLASS, children: l.signUpText })] }))] }));
243
255
  }
@@ -23,8 +23,17 @@ export interface RegisterFormLabels {
23
23
  submittingButton?: string;
24
24
  hasAccountText?: string;
25
25
  signInText?: string;
26
- /** Divider label between social sign-up and email/password (defaults to "or") */
26
+ /**
27
+ * Divider label between the social provider buttons and the email/password
28
+ * form (defaults to "or continue with email"). Rendered only when the server
29
+ * reports at least one social provider.
30
+ */
27
31
  orText?: string;
32
+ /**
33
+ * Social provider button label (defaults to "Sign up with {provider}");
34
+ * `{provider}` is replaced with the provider's display name.
35
+ */
36
+ socialButton?: string;
28
37
  }
29
38
  export interface RegisterFormProps {
30
39
  /** Callback on successful registration AND auto sign-in. Not called when the
@@ -34,6 +43,16 @@ export interface RegisterFormProps {
34
43
  * sign-in on email verification. The page should swap the form for a
35
44
  * "check your inbox" confirmation. */
36
45
  onVerificationRequired?: (email: string) => void;
46
+ /**
47
+ * Where the email-verification link should land once the address is
48
+ * verified — e.g. the invitation page the user registered from
49
+ * (objectui#10893). Forwarded to `signUp` as better-auth's sign-up
50
+ * `callbackURL`; see `SignUpData.callbackURL` for the only shapes the server
51
+ * accepts (a document-relative `./…` fails the whole sign-up). Omitted, the
52
+ * server default (`/`) applies. Only matters when the server requires email
53
+ * verification — an auto-signed-in registration calls `onSuccess` instead.
54
+ */
55
+ verificationCallbackURL?: string;
37
56
  /** Callback on registration error */
38
57
  onError?: (error: Error) => void;
39
58
  /** Link to login page */
@@ -70,5 +89,5 @@ export interface RegisterFormProps {
70
89
  * />
71
90
  * ```
72
91
  */
73
- export declare function RegisterForm({ onSuccess, onVerificationRequired, onError, loginUrl, title, description, icon, hideIcon, linkComponent: LinkComp, labels, errorMessages, }: RegisterFormProps): React.JSX.Element;
92
+ export declare function RegisterForm({ onSuccess, onVerificationRequired, verificationCallbackURL, onError, loginUrl, title, description, icon, hideIcon, linkComponent: LinkComp, labels, errorMessages, }: RegisterFormProps): React.JSX.Element;
74
93
  //# sourceMappingURL=RegisterForm.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RegisterForm.d.ts","sourceRoot":"","sources":["../src/RegisterForm.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAWzD,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC;sFACkF;IAClF,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB;;2CAEuC;IACvC,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,qCAAqC;IACrC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,yBAAyB;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;IAC5D,uCAAuC;IACvC,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAwBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,EAC3B,SAAS,EACT,sBAAsB,EACtB,OAAO,EACP,QAAmB,EACnB,KAA2B,EAC3B,WAAqD,EACrD,IAAI,EACJ,QAAgB,EAChB,aAAa,EAAE,QAAsB,EACrC,MAAW,EACX,aAAa,GACd,EAAE,iBAAiB,qBAuKnB"}
1
+ {"version":3,"file":"RegisterForm.d.ts","sourceRoot":"","sources":["../src/RegisterForm.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAWzD,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC;sFACkF;IAClF,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB;;2CAEuC;IACvC,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD;;;;;;;;OAQG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,qCAAqC;IACrC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,yBAAyB;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;IAC5D,uCAAuC;IACvC,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAwBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,EAC3B,SAAS,EACT,sBAAsB,EACtB,uBAAuB,EACvB,OAAO,EACP,QAAmB,EACnB,KAA2B,EAC3B,WAAqD,EACrD,IAAI,EACJ,QAAgB,EAChB,aAAa,EAAE,QAAsB,EACrC,MAAW,EACX,aAAa,GACd,EAAE,iBAAiB,qBA4KnB"}
@@ -25,7 +25,7 @@ const DefaultUserPlusIcon = () => (_jsxs("svg", { viewBox: "0 0 24 24", fill: "n
25
25
  * />
26
26
  * ```
27
27
  */
28
- export function RegisterForm({ onSuccess, onVerificationRequired, onError, loginUrl = '/login', title = 'Create an account', description = 'Enter your information to get started', icon, hideIcon = false, linkComponent: LinkComp = DefaultLink, labels = {}, errorMessages, }) {
28
+ export function RegisterForm({ onSuccess, onVerificationRequired, verificationCallbackURL, onError, loginUrl = '/login', title = 'Create an account', description = 'Enter your information to get started', icon, hideIcon = false, linkComponent: LinkComp = DefaultLink, labels = {}, errorMessages, }) {
29
29
  const { signUp, isLoading } = useAuth();
30
30
  const [name, setName] = useState('');
31
31
  const [email, setEmail] = useState('');
@@ -47,7 +47,6 @@ export function RegisterForm({ onSuccess, onVerificationRequired, onError, login
47
47
  submittingButton: labels.submittingButton ?? 'Creating account…',
48
48
  hasAccountText: labels.hasAccountText ?? 'Already have an account?',
49
49
  signInText: labels.signInText ?? 'Sign in',
50
- orText: labels.orText ?? 'or',
51
50
  };
52
51
  const handleSubmit = async (e) => {
53
52
  e.preventDefault();
@@ -64,7 +63,7 @@ export function RegisterForm({ onSuccess, onVerificationRequired, onError, login
64
63
  // that the server rejects as "Invalid email" (#3238) — trim before use.
65
64
  const trimmedEmail = email.trim();
66
65
  try {
67
- const result = await signUp(name, trimmedEmail, password);
66
+ const result = await signUp(name, trimmedEmail, password, verificationCallbackURL);
68
67
  if (result?.requiresVerification) {
69
68
  onVerificationRequired?.(trimmedEmail);
70
69
  return;
@@ -78,5 +77,5 @@ export function RegisterForm({ onSuccess, onVerificationRequired, onError, login
78
77
  onError?.(authError);
79
78
  }
80
79
  };
81
- return (_jsxs("div", { className: "mx-auto flex w-full flex-col justify-center space-y-7 sm:w-[400px]", children: [_jsx(AuthFormHeader, { icon: hideIcon ? undefined : (icon ?? _jsx(DefaultUserPlusIcon, {})), title: title, description: description }), _jsxs("div", { className: "space-y-5", children: [_jsx(SocialSignInButtons, { mode: "sign-up" }), _jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [error && _jsx(AuthErrorBanner, { message: error }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "register-name", className: AUTH_FIELD_LABEL_CLASS, children: l.nameLabel }), _jsx("input", { id: "register-name", type: "text", placeholder: l.namePlaceholder, value: name, onChange: (e) => setName(e.target.value), required: true, autoComplete: "name", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "register-email", className: AUTH_FIELD_LABEL_CLASS, children: l.emailLabel }), _jsx("input", { id: "register-email", type: "email", placeholder: l.emailPlaceholder, value: email, onChange: (e) => setEmail(e.target.value), required: true, autoComplete: "email", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "register-password", className: AUTH_FIELD_LABEL_CLASS, children: l.passwordLabel }), _jsx("input", { id: "register-password", type: "password", placeholder: l.passwordPlaceholder, value: password, onChange: (e) => setPassword(e.target.value), required: true, minLength: 8, autoComplete: "new-password", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "register-confirm-password", className: AUTH_FIELD_LABEL_CLASS, children: l.confirmPasswordLabel }), _jsx("input", { id: "register-confirm-password", type: "password", placeholder: l.confirmPasswordPlaceholder, value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), required: true, minLength: 8, autoComplete: "new-password", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("button", { type: "submit", disabled: isLoading, className: AUTH_PRIMARY_BUTTON_CLASS, children: [isLoading && _jsx(AuthSpinner, {}), isLoading ? l.submittingButton : l.submitButton] })] })] }), loginUrl && (_jsxs("p", { className: "px-8 text-center text-sm text-muted-foreground", children: [l.hasAccountText, ' ', _jsx(LinkComp, { href: loginUrl, className: AUTH_LINK_CLASS, children: l.signInText })] }))] }));
80
+ return (_jsxs("div", { className: "mx-auto flex w-full flex-col justify-center space-y-7 sm:w-[400px]", children: [_jsx(AuthFormHeader, { icon: hideIcon ? undefined : (icon ?? _jsx(DefaultUserPlusIcon, {})), title: title, description: description }), _jsxs("div", { className: "space-y-5", children: [_jsx(SocialSignInButtons, { mode: "sign-up", dividerText: labels.orText, buttonText: labels.socialButton }), _jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [error && _jsx(AuthErrorBanner, { message: error }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "register-name", className: AUTH_FIELD_LABEL_CLASS, children: l.nameLabel }), _jsx("input", { id: "register-name", type: "text", placeholder: l.namePlaceholder, value: name, onChange: (e) => setName(e.target.value), required: true, autoComplete: "name", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "register-email", className: AUTH_FIELD_LABEL_CLASS, children: l.emailLabel }), _jsx("input", { id: "register-email", type: "email", placeholder: l.emailPlaceholder, value: email, onChange: (e) => setEmail(e.target.value), required: true, autoComplete: "email", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "register-password", className: AUTH_FIELD_LABEL_CLASS, children: l.passwordLabel }), _jsx("input", { id: "register-password", type: "password", placeholder: l.passwordPlaceholder, value: password, onChange: (e) => setPassword(e.target.value), required: true, minLength: 8, autoComplete: "new-password", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "register-confirm-password", className: AUTH_FIELD_LABEL_CLASS, children: l.confirmPasswordLabel }), _jsx("input", { id: "register-confirm-password", type: "password", placeholder: l.confirmPasswordPlaceholder, value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), required: true, minLength: 8, autoComplete: "new-password", disabled: isLoading, className: AUTH_INPUT_CLASS })] }), _jsxs("button", { type: "submit", disabled: isLoading, className: AUTH_PRIMARY_BUTTON_CLASS, children: [isLoading && _jsx(AuthSpinner, {}), isLoading ? l.submittingButton : l.submitButton] })] })] }), loginUrl && (_jsxs("p", { className: "px-8 text-center text-sm text-muted-foreground", children: [l.hasAccountText, ' ', _jsx(LinkComp, { href: loginUrl, className: AUTH_LINK_CLASS, children: l.signInText })] }))] }));
82
81
  }
@@ -13,8 +13,21 @@ export interface SocialSignInButtonsProps {
13
13
  callbackURL?: string;
14
14
  /** Where the provider should redirect on error. Defaults to current page. */
15
15
  errorCallbackURL?: string;
16
- /** Divider text shown between social buttons and the email form */
16
+ /** Divider text shown between social buttons and the email form (defaults to "or continue with email") */
17
17
  dividerText?: string;
18
+ /**
19
+ * Omit the divider under the buttons. For a host that draws no email form
20
+ * beneath them at that moment, e.g. `LoginForm` under SSO-only mode until
21
+ * the password form is revealed. Defaults to false: the divider renders.
22
+ */
23
+ hideDivider?: boolean;
24
+ /**
25
+ * Provider button label, for i18n. `{provider}` is replaced with the
26
+ * provider's display name, so a translation places the name where its
27
+ * grammar needs it. Defaults to "Continue with {provider}" in `sign-in`
28
+ * mode and "Sign up with {provider}" in `sign-up` mode.
29
+ */
30
+ buttonText?: string;
18
31
  /** Called once after config resolves with whether any social providers are available. */
19
32
  onProvidersResolved?: (hasProviders: boolean) => void;
20
33
  }
@@ -25,5 +38,5 @@ export interface SocialSignInButtonsProps {
25
38
  *
26
39
  * Returns `null` while loading or when the server reports no providers.
27
40
  */
28
- export declare function SocialSignInButtons({ mode, callbackURL, errorCallbackURL, dividerText, onProvidersResolved, }: SocialSignInButtonsProps): React.JSX.Element | null;
41
+ export declare function SocialSignInButtons({ mode, callbackURL, errorCallbackURL, dividerText, hideDivider, buttonText, onProvidersResolved, }: SocialSignInButtonsProps): React.JSX.Element | null;
29
42
  //# sourceMappingURL=SocialSignInButtons.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SocialSignInButtons.d.ts","sourceRoot":"","sources":["../src/SocialSignInButtons.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAkFnD,MAAM,WAAW,wBAAwB;IACvC,sFAAsF;IACtF,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC7B,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,mBAAmB,CAAC,EAAE,CAAC,YAAY,EAAE,OAAO,KAAK,IAAI,CAAC;CACvD;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,IAAgB,EAChB,WAAW,EACX,gBAAgB,EAChB,WAAsC,EACtC,mBAAmB,GACpB,EAAE,wBAAwB,4BAyF1B"}
1
+ {"version":3,"file":"SocialSignInButtons.d.ts","sourceRoot":"","sources":["../src/SocialSignInButtons.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAkFnD,MAAM,WAAW,wBAAwB;IACvC,sFAAsF;IACtF,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC7B,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,0GAA0G;IAC1G,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,mBAAmB,CAAC,EAAE,CAAC,YAAY,EAAE,OAAO,KAAK,IAAI,CAAC;CACvD;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,IAAgB,EAChB,WAAW,EACX,gBAAgB,EAChB,WAAsC,EACtC,WAAmB,EACnB,UAAU,EACV,mBAAmB,GACpB,EAAE,wBAAwB,4BA6F1B"}
@@ -48,7 +48,7 @@ function ProviderIcon({ id }) {
48
48
  *
49
49
  * Returns `null` while loading or when the server reports no providers.
50
50
  */
51
- export function SocialSignInButtons({ mode = 'sign-in', callbackURL, errorCallbackURL, dividerText = 'or continue with email', onProvidersResolved, }) {
51
+ export function SocialSignInButtons({ mode = 'sign-in', callbackURL, errorCallbackURL, dividerText = 'or continue with email', hideDivider = false, buttonText, onProvidersResolved, }) {
52
52
  const { getAuthConfig, signInWithProvider } = useAuth();
53
53
  const [providers, setProviders] = useState([]);
54
54
  const [loading, setLoading] = useState(true);
@@ -86,7 +86,7 @@ export function SocialSignInButtons({ mode = 'sign-in', callbackURL, errorCallba
86
86
  }, [getAuthConfig]);
87
87
  if (loading || providers.length === 0)
88
88
  return null;
89
- const label = mode === 'sign-in' ? 'Continue with' : 'Sign up with';
89
+ const labelTemplate = buttonText ?? (mode === 'sign-in' ? 'Continue with {provider}' : 'Sign up with {provider}');
90
90
  const defaultCallback = typeof window !== 'undefined' ? window.location.href : undefined;
91
91
  const onClick = async (provider) => {
92
92
  if (pendingId)
@@ -107,5 +107,5 @@ export function SocialSignInButtons({ mode = 'sign-in', callbackURL, errorCallba
107
107
  setPendingId(null);
108
108
  }
109
109
  };
110
- return (_jsxs("div", { className: "flex flex-col gap-2", children: [error && (_jsx("div", { className: "rounded-md bg-destructive/15 p-3 text-sm text-destructive", role: "alert", children: error })), providers.map((p) => (_jsxs("button", { type: "button", onClick: () => onClick(p), disabled: pendingId !== null, "aria-busy": pendingId === p.id, className: "inline-flex h-10 w-full items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", children: [pendingId === p.id ? _jsx(AuthSpinner, {}) : _jsx(ProviderIcon, { id: p.id }), label, " ", PROVIDER_LABEL[p.id] ?? p.name] }, p.id))), _jsxs("div", { className: "relative my-1", children: [_jsx("div", { className: "absolute inset-0 flex items-center", children: _jsx("span", { className: "w-full border-t" }) }), _jsx("div", { className: "relative flex justify-center text-xs uppercase", children: _jsx("span", { className: "bg-background px-2 text-muted-foreground", children: dividerText }) })] })] }));
110
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [error && (_jsx("div", { className: "rounded-md bg-destructive/15 p-3 text-sm text-destructive", role: "alert", children: error })), providers.map((p) => (_jsxs("button", { type: "button", onClick: () => onClick(p), disabled: pendingId !== null, "aria-busy": pendingId === p.id, className: "inline-flex h-10 w-full items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", children: [pendingId === p.id ? _jsx(AuthSpinner, {}) : _jsx(ProviderIcon, { id: p.id }), labelTemplate.replace('{provider}', () => PROVIDER_LABEL[p.id] ?? p.name)] }, p.id))), hideDivider ? null : (_jsxs("div", { className: "relative my-1", children: [_jsx("div", { className: "absolute inset-0 flex items-center", children: _jsx("span", { className: "w-full border-t" }) }), _jsx("div", { className: "relative flex justify-center text-xs uppercase", children: _jsx("span", { className: "bg-background px-2 text-muted-foreground", children: dividerText }) })] }))] }));
111
111
  }
@@ -21,7 +21,7 @@ export interface UserMenuProps {
21
21
  * Shows avatar, name, email, and common actions (profile, settings, sign out).
22
22
  *
23
23
  * This is a headless component that provides the user data and actions.
24
- * The actual dropdown rendering is handled by the consumer (e.g., AppSidebar).
24
+ * The actual dropdown rendering is handled by the consumer.
25
25
  *
26
26
  * @example
27
27
  * ```tsx
package/dist/UserMenu.js CHANGED
@@ -6,7 +6,7 @@ import { getUserInitials } from './types.js';
6
6
  * Shows avatar, name, email, and common actions (profile, settings, sign out).
7
7
  *
8
8
  * This is a headless component that provides the user data and actions.
9
- * The actual dropdown rendering is handled by the consumer (e.g., AppSidebar).
9
+ * The actual dropdown rendering is handled by the consumer.
10
10
  *
11
11
  * @example
12
12
  * ```tsx
@@ -9,16 +9,18 @@
9
9
  * ADR-0069 — client side of the authentication-policy session gate.
10
10
  *
11
11
  * The ObjectStack backend returns `403 { error: { code: 'MFA_REQUIRED' |
12
- * 'PASSWORD_EXPIRED', message } }` from protected endpoints when a logged-in
13
- * user must remediate. The authenticated fetch wrapper detects this on EVERY
12
+ * 'PASSWORD_EXPIRED' | 'PASSWORD_RESET_REQUIRED', message } }` from protected
13
+ * endpoints when a logged-in user must remediate. `PASSWORD_RESET_REQUIRED` is
14
+ * an administrator-forced password change, distinct on the wire from natural
15
+ * expiry (objectstack#23287 ruling C′). The authenticated fetch wrapper detects this on EVERY
14
16
  * API response and emits it here; `AuthProvider` subscribes and raises a
15
17
  * full-screen remediation overlay. Decoupling via a tiny module-level emitter
16
18
  * keeps the plain fetch wrapper free of React.
17
19
  */
18
20
  /** A triggered auth-policy gate the user must clear before continuing. */
19
21
  export interface AuthGateInfo {
20
- /** Stable machine code, e.g. `MFA_REQUIRED` / `PASSWORD_EXPIRED`. */
21
- code: 'MFA_REQUIRED' | 'PASSWORD_EXPIRED' | string;
22
+ /** Stable machine code: one of the codes `detectAuthGate` recognises. */
23
+ code: 'MFA_REQUIRED' | 'PASSWORD_EXPIRED' | 'PASSWORD_RESET_REQUIRED' | string;
22
24
  /** Human-facing message from the server (may be empty). */
23
25
  message: string;
24
26
  }