@ciromaciel/auth-react 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -77,7 +77,10 @@ Or use the ready-made screen:
77
77
  authenticatedRedirect="/" // where to send someone who already has a session
78
78
  onCodeSent={email => notify(`Code sent to ${email}`)}
79
79
  onSuccess={(user, { result, redirectHandled }) => notify(`Welcome ${user?.email}`)}
80
- onError={error => notify(error.message)} // error.code carries the stable identifier
80
+ onError={(error, { isShownOnCard }) => {
81
+ // A wrong, expired or exhausted code is already explained on the card
82
+ if (!isShownOnCard) notify(error.message) // error.code carries the stable identifier
83
+ }}
81
84
  />
82
85
  ```
83
86
 
@@ -87,6 +90,10 @@ Or use the ready-made screen:
87
90
  `redirectOrigins` adds domains and `handleRedirect={false}` hands control back to the app.
88
91
  - A wrong code fails with `error.details.attemptsLeft`. Five wrong tries destroy the request and
89
92
  the person asks for a new code.
93
+ - On the code step the card explains every failure itself: how many tries are left, or, once the
94
+ code is exhausted or expired, a button that sends a new one. `onError` still fires, with a second
95
+ argument `{ step: 'request' | 'verify', isShownOnCard }`; skip your own notification when
96
+ `isShownOnCard` is `true`, or the person reads the same failure twice.
90
97
 
91
98
  ### Social sign-in
92
99
 
@@ -114,18 +121,23 @@ email form.
114
121
  - An account is saved only after a session exists: the code confirmed, or the provider's token
115
122
  back. A mistyped email never becomes a suggestion.
116
123
  - The list keeps the five most recent accounts. Each entry holds the email, the way in and the
117
- time of the last sign-in. It lives in `localStorage` under `auth:recent-accounts`, so each
118
- origin keeps its own list.
119
- - Signing out keeps the list. "Gerenciar" on the screen removes an account; nothing else about
120
- that account changes.
124
+ time of the last sign-in.
125
+ - Every app of the same application shares the list, even across origins: the Auth worker keeps
126
+ it in an HttpOnly cookie on its own host and answers only to the origins the application
127
+ allows. A copy in `localStorage` (`auth:recent-accounts`) renders at once while the shared list
128
+ loads, and stands in when the worker cannot be reached.
129
+ - Signing out keeps the list. "Gerenciar" on the screen removes an account from every app of the
130
+ application; nothing else about that account changes.
121
131
 
122
132
  ```jsx
123
133
  <SignIn /> {/* shows and saves recent accounts */}
124
134
  <SignIn recentAccounts={false} /> {/* shared computers: neither shows nor saves */}
125
135
  ```
126
136
 
127
- Building your own screen? `listRecentAccounts()`, `rememberAccount(email, method)` and
128
- `forgetAccount(email)` read and write the same list.
137
+ Building your own screen? `fetchRecentAccounts()`, `saveRecentAccount(method)` and
138
+ `deleteRecentAccount(email)` talk to the shared list; `listRecentAccounts()`,
139
+ `rememberAccount(email, method)`, `forgetAccount(email)` and `adoptRecentAccounts(list)` manage
140
+ the local copy.
129
141
 
130
142
  ## API
131
143
 
package/dist/index.esm.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { create } from 'zustand';
2
- import { useState, useCallback, useEffect, useMemo, createContext } from 'react';
2
+ import { useState, useCallback, useEffect, useMemo, createContext, useRef } from 'react';
3
3
  import { useShallow } from 'zustand/react/shallow';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import { Navigate, Outlet, useNavigate } from 'react-router-dom';
6
- import { Modal, Stack, Text, Group, Image, Title, Paper, Anchor, NavLink, ActionIcon, Loader, Avatar, Button, Divider, TextInput, Center, Box, Collapse, FileButton, Tooltip, ThemeIcon, Badge, rem } from '@mantine/core';
6
+ import { Modal, Stack, Text, Group, Image, Title, Paper, Anchor, NavLink, ActionIcon, Loader, Avatar, Button, Divider, TextInput, Alert, Center, Box, Collapse, FileButton, Tooltip, ThemeIcon, Badge, rem } from '@mantine/core';
7
7
  import { useForm } from '@mantine/form';
8
- import { IconX, IconArrowRight, IconBrandGoogle, IconArrowLeft, IconUser, IconPhoto, IconTrash, IconCheck, IconPencil, IconMail, IconShield, IconDevices, IconDeviceMobile, IconLogout, IconUserCircle, IconSettings, IconCreditCard, IconShieldCheck } from '@tabler/icons-react';
8
+ import { IconX, IconArrowRight, IconBrandGoogle, IconArrowLeft, IconRefresh, IconAlertCircle, IconUser, IconPhoto, IconTrash, IconCheck, IconPencil, IconMail, IconShield, IconDevices, IconDeviceMobile, IconLogout, IconUserCircle, IconSettings, IconCreditCard, IconShieldCheck } from '@tabler/icons-react';
9
9
 
10
10
  /**
11
11
  * The accounts that already signed in on this browser.
@@ -110,6 +110,28 @@ function forgetAccount(email) {
110
110
  return next;
111
111
  }
112
112
 
113
+ /**
114
+ * Replaces the local copy with the list the worker returned.
115
+ *
116
+ * The worker's list is the one every panel shares, so when it has accounts it
117
+ * wins: an account removed on another panel must not come back from this
118
+ * panel's stale copy. An EMPTY answer does not wipe the local one — it is what
119
+ * a browser that signed in before the shared list existed gets, and those
120
+ * shortcuts are still true.
121
+ *
122
+ * @returns the list to show
123
+ */
124
+ function adoptRecentAccounts(remote) {
125
+ if (!Array.isArray(remote) || remote.length === 0) return listRecentAccounts();
126
+ const next = remote.filter(account => account && typeof account.email === 'string' && account.email.includes('@')).map(account => ({
127
+ email: normalizeEmail(account.email),
128
+ method: typeof account.method === 'string' && account.method ? account.method : 'code',
129
+ lastUsedAt: Number(account.lastUsedAt) || 0
130
+ })).sort((a, b) => b.lastUsedAt - a.lastUsedAt).slice(0, MAX_RECENT_ACCOUNTS);
131
+ writeRecentAccounts(next);
132
+ return next;
133
+ }
134
+
113
135
  /** Records the provider this tab is leaving for. */
114
136
  function markSocialDeparture(provider) {
115
137
  try {
@@ -691,6 +713,64 @@ const updateProfile = async data => {
691
713
  });
692
714
  };
693
715
 
716
+ /*--- Recent accounts ------------------------------------------------------*/
717
+
718
+ /*
719
+ * The server's copy of this browser's sign-in shortcuts.
720
+ *
721
+ * `localStorage` is per origin, so a list kept only there stayed on the panel
722
+ * where the person signed in. The worker keeps it in an HttpOnly cookie on its
723
+ * own host, which every panel of the application reaches — and answers only to
724
+ * the origins the application allows (`routes/recent-accounts.js`).
725
+ *
726
+ * All three fail soft: the shortcuts are a convenience, and a network error or
727
+ * an origin the worker refuses must never cost the sign-in. `null` means "no
728
+ * answer", which the screen reads as "keep what you have".
729
+ */
730
+
731
+ /** The list for this application, or `null` when the worker did not answer. */
732
+ const fetchRecentAccounts = async () => {
733
+ try {
734
+ const response = await api('/auth/recent-accounts');
735
+ return Array.isArray(response?.items) ? response.items : null;
736
+ } catch {
737
+ return null;
738
+ }
739
+ };
740
+
741
+ /**
742
+ * Records the account of the CURRENT session. The worker reads the email from
743
+ * the session, never from here. `keepalive` because the screen is usually
744
+ * navigating away at this very moment.
745
+ */
746
+ const saveRecentAccount = async (method = 'code') => {
747
+ try {
748
+ const response = await api('/auth/recent-accounts', {
749
+ method: 'POST',
750
+ body: JSON.stringify({
751
+ method
752
+ }),
753
+ keepalive: true
754
+ });
755
+ return Array.isArray(response?.items) ? response.items : null;
756
+ } catch {
757
+ return null;
758
+ }
759
+ };
760
+
761
+ /** Takes one account off this browser's list, on every panel. */
762
+ const deleteRecentAccount = async email => {
763
+ try {
764
+ await api(`/auth/recent-accounts/${encodeURIComponent(email)}`, {
765
+ method: 'DELETE',
766
+ keepalive: true
767
+ });
768
+ return true;
769
+ } catch {
770
+ return false;
771
+ }
772
+ };
773
+
694
774
  /*--- Social sign-in -------------------------------------------------------*/
695
775
 
696
776
  /**
@@ -799,6 +879,9 @@ const consumeSocialToken = () => {
799
879
  if (provider) {
800
880
  const email = decodeJWT(token)?.email;
801
881
  if (email) rememberAccount(email, provider);
882
+ // The shared copy, so the other panels learn it too. Not awaited: the
883
+ // token is already stored, and the sign-in must not wait on a shortcut.
884
+ saveRecentAccount(provider);
802
885
  }
803
886
  params.delete('token');
804
887
  const rest = params.toString();
@@ -2518,6 +2601,102 @@ function TermsNotice({
2518
2601
  });
2519
2602
  }
2520
2603
 
2604
+ /**
2605
+ * What a failed `verify` means for the person, read from the worker's answer.
2606
+ *
2607
+ * The HTTP status carries the case — `code` is `VALIDATION_ERROR` for both a
2608
+ * wrong and an expired code, so it cannot tell them apart:
2609
+ * - 400 with `details.attemptsLeft`: wrong code, the request is still open;
2610
+ * - 400 without it: no open request for this email (already replaced);
2611
+ * - 429: the fifth wrong try destroyed the request;
2612
+ * - 410: the code outlived its minutes, and was destroyed too.
2613
+ *
2614
+ * Anything else — the network, a 500 — is not about the code, and must not
2615
+ * lock the field.
2616
+ */
2617
+ function describeCodeFailure(error) {
2618
+ if (error?.status === 429) return {
2619
+ kind: 'exhausted',
2620
+ isLocked: true
2621
+ };
2622
+ if (error?.status === 410) return {
2623
+ kind: 'expired',
2624
+ isLocked: true
2625
+ };
2626
+ if (error?.status === 400) {
2627
+ const attemptsLeft = error?.details?.attemptsLeft;
2628
+ return {
2629
+ kind: 'wrong',
2630
+ isLocked: false,
2631
+ attemptsLeft: Number.isInteger(attemptsLeft) ? attemptsLeft : null
2632
+ };
2633
+ }
2634
+ return {
2635
+ kind: 'other',
2636
+ isLocked: false,
2637
+ message: error?.message || null
2638
+ };
2639
+ }
2640
+
2641
+ /**
2642
+ * The notice above the code field.
2643
+ *
2644
+ * It sits ABOVE the field, not under it, and says what to do next — not only
2645
+ * that something failed. The field's own error line was 12px of red under a
2646
+ * cleared input, next to a greyed-out button: it read as a frozen screen.
2647
+ *
2648
+ * The most common cause gets named: a new code invalidates the previous one,
2649
+ * and the person is often reading an older email.
2650
+ */
2651
+ function CodeFailureNotice({
2652
+ failure,
2653
+ labels
2654
+ }) {
2655
+ if (!failure) return null;
2656
+ const texts = {
2657
+ wrong: {
2658
+ title: labels.wrongCodeTitle || 'Código incorreto',
2659
+ body: [labels.wrongCodeHint || 'Confira o e-mail mais recente: um código novo invalida o anterior.', failure.attemptsLeft === 1 ? labels.lastAttempt || 'Esta é a última tentativa.' : failure.attemptsLeft > 1 ? labels.attemptsLeft ? labels.attemptsLeft(failure.attemptsLeft) : `Restam ${failure.attemptsLeft} tentativas.` : null].filter(Boolean).join(' ')
2660
+ },
2661
+ exhausted: {
2662
+ title: labels.attemptsExhaustedTitle || 'Tentativas esgotadas',
2663
+ body: labels.attemptsExhausted || 'Por segurança, este código foi cancelado. Peça um novo para continuar.'
2664
+ },
2665
+ expired: {
2666
+ title: labels.codeExpiredTitle || 'Código expirado',
2667
+ body: labels.codeExpired || 'O código vale por poucos minutos. Peça um novo para continuar.'
2668
+ },
2669
+ other: {
2670
+ title: labels.codeFailedTitle || 'Não foi possível entrar',
2671
+ body: failure.message || labels.invalidCode || 'Tente de novo em instantes.'
2672
+ }
2673
+ }[failure.kind];
2674
+ return /*#__PURE__*/jsx(Alert, {
2675
+ color: "red",
2676
+ variant: "light",
2677
+ radius: 0,
2678
+ icon: /*#__PURE__*/jsx(IconAlertCircle, {
2679
+ size: 18
2680
+ }),
2681
+ title: texts.title
2682
+ /*
2683
+ * `role="alert"` is Mantine's default and is what makes a screen
2684
+ * reader announce the failure without the person moving focus away
2685
+ * from the field they are about to retype in.
2686
+ */,
2687
+ styles: {
2688
+ root: {
2689
+ border: '1px solid var(--mantine-color-red-2)'
2690
+ }
2691
+ },
2692
+ children: /*#__PURE__*/jsx(Text, {
2693
+ size: "sm",
2694
+ lh: 1.45,
2695
+ children: texts.body
2696
+ })
2697
+ });
2698
+ }
2699
+
2521
2700
  // The OAuth flow's pass-through screen.
2522
2701
  //
2523
2702
  // The panel is not the destination here: the user is authorizing an MCP client
@@ -2630,7 +2809,9 @@ function SignIn({
2630
2809
  // present it again: it is the (email, code) pair the server validates.
2631
2810
  const [sentTo, setSentTo] = useState(null);
2632
2811
  const [code, setCode] = useState('');
2633
- const [codeError, setCodeError] = useState(null);
2812
+ const [codeFailure, setCodeFailure] = useState(null);
2813
+ const [isCodeResent, setIsCodeResent] = useState(false);
2814
+ const codeInputRef = useRef(null);
2634
2815
 
2635
2816
  // Read once, on mount: the list only changes through this screen, and
2636
2817
  // every change below writes the new list back into state.
@@ -2639,6 +2820,7 @@ function SignIn({
2639
2820
  const [isManaging, setIsManaging] = useState(false);
2640
2821
  const [pickingEmail, setPickingEmail] = useState(null);
2641
2822
  const isShowingAccounts = recentAccounts && accounts.length > 0 && !isChoosingOther;
2823
+ const isCodeLocked = !!codeFailure?.isLocked;
2642
2824
 
2643
2825
  // Hook that fetches the application's logo
2644
2826
  const applicationLogo = useApplicationLogo();
@@ -2683,20 +2865,64 @@ function SignIn({
2683
2865
  // eslint-disable-next-line react-hooks/exhaustive-deps -- redirectOrigins enters through the serialized key above
2684
2866
  }, [authLoading, user, authenticatedRedirect, handleRedirect, redirectOriginsKey, navigate]);
2685
2867
 
2868
+ /*
2869
+ * The shared list, from the worker.
2870
+ *
2871
+ * The local copy renders at once; this replaces it when the answer
2872
+ * arrives. That is what makes an account used on the Auth panel appear
2873
+ * on Hoster: `localStorage` never crosses between the two origins.
2874
+ *
2875
+ * If the person has already started typing an email, the list does not
2876
+ * yank the form away from under them — it only feeds the "Contas salvas"
2877
+ * link, one click away.
2878
+ */
2879
+ useEffect(() => {
2880
+ if (!recentAccounts) return;
2881
+ let isActive = true;
2882
+ fetchRecentAccounts().then(remote => {
2883
+ if (!isActive || remote === null) return;
2884
+ const next = adoptRecentAccounts(remote);
2885
+ if (form.isDirty()) setIsChoosingOther(true);
2886
+ setAccounts(next);
2887
+ });
2888
+ return () => {
2889
+ isActive = false;
2890
+ };
2891
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- once per mount, like the local read
2892
+ }, [recentAccounts]);
2893
+
2686
2894
  // Step 1 — ask for the code.
2687
2895
  const handleRequest = async values => {
2688
- if (sending) return;
2896
+ if (sending) return false;
2689
2897
  try {
2690
2898
  await requestCode(values.email);
2691
2899
  setSentTo(values.email);
2692
2900
  setCode('');
2693
- setCodeError(null);
2901
+ setCodeFailure(null);
2694
2902
  onCodeSent?.(values.email);
2903
+ return true;
2695
2904
  } catch (error) {
2696
- onError?.(error);
2905
+ // Nothing on the card shows this one: the app's notification is
2906
+ // the only place the person learns the code was not sent.
2907
+ onError?.(error, {
2908
+ step: 'request',
2909
+ isShownOnCard: false
2910
+ });
2911
+ return false;
2697
2912
  }
2698
2913
  };
2699
2914
 
2915
+ // A new code, from the code step. The worker replaces the request, so the
2916
+ // attempts start over and the previous code stops working — the notice
2917
+ // says so, or the person keeps typing the one from the older email.
2918
+ const handleResend = async () => {
2919
+ const isSent = await handleRequest({
2920
+ email: sentTo
2921
+ });
2922
+ setIsCodeResent(isSent);
2923
+ if (isSent) codeInputRef.current?.focus();
2924
+ };
2925
+
2700
2926
  // Step 1, from the list — the click on a saved account IS the request.
2701
2927
  //
2702
2928
  // An account that came in through a provider goes back to that provider,
@@ -2724,6 +2950,9 @@ function SignIn({
2724
2950
  const handleForget = account => {
2725
2951
  const next = forgetAccount(account.email);
2726
2952
  setAccounts(next);
2953
+ // On every panel, not just this one. The local removal above already
2954
+ // took it off this screen, so a failure here costs nothing visible.
2955
+ deleteRecentAccount(account.email);
2727
2956
  if (next.length === 0) setIsManaging(false);
2728
2957
  };
2729
2958
 
@@ -2744,14 +2973,18 @@ function SignIn({
2744
2973
  // signals that navigation was taken over — now as information, not as a
2745
2974
  // trap.
2746
2975
  const handleVerify = async value => {
2747
- setCodeError(null);
2976
+ setCodeFailure(null);
2977
+ setIsCodeResent(false);
2748
2978
  try {
2749
2979
  const result = await verifyCode(sentTo, value);
2750
2980
 
2751
2981
  // Only now, with a session: an email that never received a valid
2752
2982
  // code never becomes a suggestion. Written before the redirect,
2753
2983
  // which may unmount this screen.
2754
- if (recentAccounts) setAccounts(rememberAccount(sentTo, 'code'));
2984
+ if (recentAccounts) {
2985
+ setAccounts(rememberAccount(sentTo, 'code'));
2986
+ saveRecentAccount('code');
2987
+ }
2755
2988
  const target = handleRedirect ? getRedirectFromLocation(redirectOrigins) : null;
2756
2989
  if (target) applyRedirect(target, navigate);
2757
2990
  onSuccess?.(result?.user ?? null, {
@@ -2759,12 +2992,19 @@ function SignIn({
2759
2992
  redirectHandled: !!target
2760
2993
  });
2761
2994
  } catch (error) {
2762
- // The code error belongs to the field, not to the global
2763
- // notification: the person is looking at the eight characters they
2764
- // just typed.
2765
- setCodeError(error?.message || labels.invalidCode || 'Código inválido.');
2995
+ // The failure belongs to this card, not to the global notification:
2996
+ // the person is looking at the eight characters they just typed.
2997
+ // The field goes back empty and focused, ready for the next try.
2998
+ setCodeFailure(describeCodeFailure(error));
2766
2999
  setCode('');
2767
- onError?.(error);
3000
+ codeInputRef.current?.focus();
3001
+ // Still reported — an app may log it — but flagged: the card
3002
+ // already explains it, and a notification repeating "Código
3003
+ // inválido" in the corner would say it twice.
3004
+ onError?.(error, {
3005
+ step: 'verify',
3006
+ isShownOnCard: true
3007
+ });
2768
3008
  }
2769
3009
  };
2770
3010
 
@@ -2874,7 +3114,27 @@ function SignIn({
2874
3114
  })
2875
3115
  }) : /*#__PURE__*/jsxs(Stack, {
2876
3116
  gap: "md",
2877
- children: [/*#__PURE__*/jsx(TextInput, {
3117
+ children: [isCodeResent && /*#__PURE__*/jsx(Alert, {
3118
+ color: "gray",
3119
+ variant: "light",
3120
+ radius: 0,
3121
+ p: "xs",
3122
+ children: /*#__PURE__*/jsxs(Text, {
3123
+ size: "xs",
3124
+ lh: 1.4,
3125
+ children: [/*#__PURE__*/jsx(Text, {
3126
+ span: true,
3127
+ inherit: true,
3128
+ fw: 700,
3129
+ c: "gray.9",
3130
+ children: labels.codeResentTitle || 'Novo código enviado.'
3131
+ }), ' ', labels.codeResent || 'O anterior deixou de valer.']
3132
+ })
3133
+ }), /*#__PURE__*/jsx(CodeFailureNotice, {
3134
+ failure: codeFailure,
3135
+ labels: labels
3136
+ }), /*#__PURE__*/jsx(TextInput, {
3137
+ ref: codeInputRef,
2878
3138
  label: labels.codeLabel || 'Código de acesso'
2879
3139
  /*
2880
3140
  * The email is the field's description, not the subtitle:
@@ -2894,26 +3154,56 @@ function SignIn({
2894
3154
  value: code,
2895
3155
  onChange: event => {
2896
3156
  setCode(event.currentTarget.value);
2897
- if (codeError) setCodeError(null);
3157
+ // Typing again is the correction: the notice has
3158
+ // done its job. A locked field cannot be typed in,
3159
+ // so an exhausted or expired notice stays.
3160
+ if (codeFailure) setCodeFailure(null);
2898
3161
  },
2899
3162
  onKeyDown: event => {
2900
3163
  if (event.key === 'Enter' && code.trim()) handleVerify(code);
2901
3164
  },
2902
3165
  autoFocus: true,
2903
3166
  autoComplete: "one-time-code",
2904
- readOnly: verifying,
2905
- error: codeError
2906
- }), /*#__PURE__*/jsx(Button, {
3167
+ readOnly: verifying
3168
+ /*
3169
+ * No `error` on the field: the notice above carries the
3170
+ * failure, and a red border around an empty field turned
3171
+ * the PLACEHOLDER red — "ABCD-EFGH" read as the code the
3172
+ * person had typed. With the request gone there is
3173
+ * nothing left to type into.
3174
+ */,
3175
+ disabled: isCodeLocked
3176
+ }), isCodeLocked ?
3177
+ /*#__PURE__*/
3178
+ /*
3179
+ * The request is gone: confirming can only fail again, so
3180
+ * the one action that works takes the button's place.
3181
+ */
3182
+ jsx(Button, {
3183
+ type: "button",
3184
+ fullWidth: true,
3185
+ "aria-disabled": sending,
3186
+ onClick: sending ? undefined : handleResend,
3187
+ leftSection: sending ? /*#__PURE__*/jsx(Loader, {
3188
+ size: 14,
3189
+ color: "gray.0"
3190
+ }) : /*#__PURE__*/jsx(IconRefresh, {
3191
+ size: 16
3192
+ }),
3193
+ children: sending ? labels.sendingCode || 'Enviando…' : labels.sendNewCode || 'Enviar novo código'
3194
+ }) : /*#__PURE__*/jsx(Button, {
2907
3195
  type: "button",
2908
3196
  fullWidth: true
2909
- // Same reason as the previous step: `disabled` would fade
2910
- // the button exactly while signing in happens. With no
2911
- // code typed it stays genuinely disabled — there is no
2912
- // action in progress to hide there.
3197
+ // Same reason as the previous step: `disabled` would
3198
+ // fade the button exactly while signing in happens.
3199
+ //
3200
+ // Never disabled for an empty field either. Right
3201
+ // after a failure the field is empty on purpose, and
3202
+ // a grey button there read as a frozen screen; the
3203
+ // click sends the cursor to the field instead.
2913
3204
  ,
2914
3205
  "aria-disabled": verifying,
2915
- disabled: !code.trim(),
2916
- onClick: verifying ? undefined : () => handleVerify(code),
3206
+ onClick: verifying ? undefined : () => code.trim() ? handleVerify(code) : codeInputRef.current?.focus(),
2917
3207
  leftSection: verifying ? /*#__PURE__*/jsx(Loader, {
2918
3208
  size: 14,
2919
3209
  color: "gray.0"
@@ -2931,18 +3221,17 @@ function SignIn({
2931
3221
  onClick: () => {
2932
3222
  setSentTo(null);
2933
3223
  setCode('');
2934
- setCodeError(null);
3224
+ setCodeFailure(null);
3225
+ setIsCodeResent(false);
2935
3226
  // The label promises another email: the form,
2936
3227
  // not the list the person may have come from.
2937
3228
  setIsChoosingOther(true);
2938
3229
  },
2939
3230
  children: labels.changeEmail || 'Usar outro e-mail'
2940
- }), /*#__PURE__*/jsx(Anchor, {
3231
+ }), !isCodeLocked && /*#__PURE__*/jsx(Anchor, {
2941
3232
  size: "sm",
2942
3233
  c: "dimmed",
2943
- onClick: sending ? undefined : () => handleRequest({
2944
- email: sentTo
2945
- }),
3234
+ onClick: sending ? undefined : handleResend,
2946
3235
  children: sending ? labels.sendingCode || 'Enviando…' : labels.resendCode || 'Reenviar código'
2947
3236
  })]
2948
3237
  })]
@@ -3926,5 +4215,5 @@ function SignOutButton({
3926
4215
  });
3927
4216
  }
3928
4217
 
3929
- export { AuthCard, AuthLoaded, AuthLoading, AuthProvider, GuestOnly, IDENTITY_CHANGED_EVENT, MAX_RECENT_ACCOUNTS, Protect, RECENT_ACCOUNTS_KEY, SignIn, SignInButton, SignOutButton, SignedIn, SignedOut, SocialButtons, TOKEN_STORAGE_KEY, UserInformation, UserProfile, Wordmark, announceIdentityChange, applyRedirect, clearIdentitySwitching, configure, consumeSocialError, consumeSocialToken, decodeJWT, endImpersonation, forgetAccount, getApiUrl, getApplicationInfo, getCurrentUser, getLinkedProviders, getRedirectFromLocation, getSession, getSocialProviders, isAuthenticated, isIdentitySwitching, isInternal, listRecentAccounts, listSessions, markIdentitySwitching, pollCode, refreshToken, rememberAccount, requestCode, resolveRedirect, revokeOtherSessions, revokeSession, setStoredToken, shouldSignOutOn401, signOut, startSocialLink, startSocialSignIn, unlinkSocialProvider, updateProfile, useApplicationLogo, useAuth, useAuthLoading, useAuthStore, useCheckToken, useImpersonation, useSession, useSessions, useSignIn, useSignOut, useUser, verifyCode };
4218
+ export { AuthCard, AuthLoaded, AuthLoading, AuthProvider, GuestOnly, IDENTITY_CHANGED_EVENT, MAX_RECENT_ACCOUNTS, Protect, RECENT_ACCOUNTS_KEY, SignIn, SignInButton, SignOutButton, SignedIn, SignedOut, SocialButtons, TOKEN_STORAGE_KEY, UserInformation, UserProfile, Wordmark, adoptRecentAccounts, announceIdentityChange, applyRedirect, clearIdentitySwitching, configure, consumeSocialError, consumeSocialToken, decodeJWT, deleteRecentAccount, endImpersonation, fetchRecentAccounts, forgetAccount, getApiUrl, getApplicationInfo, getCurrentUser, getLinkedProviders, getRedirectFromLocation, getSession, getSocialProviders, isAuthenticated, isIdentitySwitching, isInternal, listRecentAccounts, listSessions, markIdentitySwitching, pollCode, refreshToken, rememberAccount, requestCode, resolveRedirect, revokeOtherSessions, revokeSession, saveRecentAccount, setStoredToken, shouldSignOutOn401, signOut, startSocialLink, startSocialSignIn, unlinkSocialProvider, updateProfile, useApplicationLogo, useAuth, useAuthLoading, useAuthStore, useCheckToken, useImpersonation, useSession, useSessions, useSignIn, useSignOut, useUser, verifyCode };
3930
4219
  //# sourceMappingURL=index.esm.js.map