@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 +19 -7
- package/dist/index.esm.js +320 -31
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +320 -27
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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={
|
|
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.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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? `
|
|
128
|
-
`
|
|
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 [
|
|
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
|
-
|
|
2901
|
+
setCodeFailure(null);
|
|
2694
2902
|
onCodeSent?.(values.email);
|
|
2903
|
+
return true;
|
|
2695
2904
|
} catch (error) {
|
|
2696
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
2763
|
-
//
|
|
2764
|
-
//
|
|
2765
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
2906
|
-
|
|
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
|
|
2910
|
-
// the button exactly while signing in happens.
|
|
2911
|
-
//
|
|
2912
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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 :
|
|
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
|