@ciromaciel/auth-react 1.0.2 → 1.2.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 +31 -1
- package/dist/index.esm.js +611 -38
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +612 -34
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -9,6 +9,143 @@ var core = require('@mantine/core');
|
|
|
9
9
|
var form = require('@mantine/form');
|
|
10
10
|
var iconsReact = require('@tabler/icons-react');
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The accounts that already signed in on this browser.
|
|
14
|
+
*
|
|
15
|
+
* `<SignIn />` offers them before the empty email field: for a returning
|
|
16
|
+
* person, choosing the account IS the click that sends the code. Nothing here
|
|
17
|
+
* is a credential — a saved email still has to receive and type a fresh code —
|
|
18
|
+
* so the list is a shortcut, never a session.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IS KEPT, AND WHEN
|
|
21
|
+
*
|
|
22
|
+
* Email, the way in (`'code'` or a provider id such as `'google'`) and the time
|
|
23
|
+
* of the last sign-in. No token, name or picture: that is everything needed to
|
|
24
|
+
* draw a row, and the rest arrives with the session.
|
|
25
|
+
*
|
|
26
|
+
* An email enters only after a session exists — the code confirmed, or the
|
|
27
|
+
* provider's token back in the fragment. Never on "send code": a typo there
|
|
28
|
+
* would otherwise become a permanent suggestion.
|
|
29
|
+
*
|
|
30
|
+
* WHY IT SURVIVES SIGN-OUT AND IDENTITY SWITCHES
|
|
31
|
+
*
|
|
32
|
+
* The list exists precisely for the person who left and is coming back, so
|
|
33
|
+
* signing out does not clear it; removing an account is an explicit action on
|
|
34
|
+
* the screen. The key is in `KEEP` in `identitySwitch.js` for the same reason:
|
|
35
|
+
* the list belongs to the browser, not to whichever account is signed in, and
|
|
36
|
+
* holds no data of any account beyond its own email.
|
|
37
|
+
*
|
|
38
|
+
* `localStorage` is per origin, so each panel keeps its own list.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const RECENT_ACCOUNTS_KEY = 'auth:recent-accounts';
|
|
42
|
+
|
|
43
|
+
// Five rows fit the 350px card with no scroll. The oldest drops out on its
|
|
44
|
+
// own; signing in with it again puts it back on top.
|
|
45
|
+
const MAX_RECENT_ACCOUNTS = 5;
|
|
46
|
+
|
|
47
|
+
// Which provider this tab left for. `sessionStorage` because the answer only
|
|
48
|
+
// matters to the tab that comes back, and it expires so an abandoned consent
|
|
49
|
+
// screen does not label a later, unrelated token.
|
|
50
|
+
const SOCIAL_DEPARTURE_KEY = 'auth:social-departure';
|
|
51
|
+
const SOCIAL_DEPARTURE_TTL_MS = 15 * 60 * 1000;
|
|
52
|
+
const normalizeEmail = email => String(email ?? '').trim().toLowerCase();
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The saved accounts, most recent first. Never throws: a private window,
|
|
56
|
+
* blocked storage or a hand-edited value all read as an empty list.
|
|
57
|
+
*
|
|
58
|
+
* @returns {{ email: string, method: string, lastUsedAt: number }[]}
|
|
59
|
+
*/
|
|
60
|
+
function listRecentAccounts() {
|
|
61
|
+
try {
|
|
62
|
+
const raw = window.localStorage.getItem(RECENT_ACCOUNTS_KEY);
|
|
63
|
+
if (!raw) return [];
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
if (!Array.isArray(parsed)) return [];
|
|
66
|
+
return parsed.filter(account => account && typeof account.email === 'string' && account.email.includes('@')).map(account => ({
|
|
67
|
+
email: normalizeEmail(account.email),
|
|
68
|
+
method: typeof account.method === 'string' && account.method ? account.method : 'code',
|
|
69
|
+
lastUsedAt: Number(account.lastUsedAt) || 0
|
|
70
|
+
})).sort((a, b) => b.lastUsedAt - a.lastUsedAt).slice(0, MAX_RECENT_ACCOUNTS);
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function writeRecentAccounts(accounts) {
|
|
76
|
+
try {
|
|
77
|
+
window.localStorage.setItem(RECENT_ACCOUNTS_KEY, JSON.stringify(accounts));
|
|
78
|
+
} catch {
|
|
79
|
+
// No storage: the screen simply keeps asking for the email.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Puts the account on top of the list, creating it or refreshing it.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} email
|
|
87
|
+
* @param {string} [method='code'] - `'code'` or the provider id
|
|
88
|
+
* @returns the list as it stands after the write
|
|
89
|
+
*/
|
|
90
|
+
function rememberAccount(email, method = 'code') {
|
|
91
|
+
const normalized = normalizeEmail(email);
|
|
92
|
+
if (!normalized.includes('@')) return listRecentAccounts();
|
|
93
|
+
const next = [{
|
|
94
|
+
email: normalized,
|
|
95
|
+
method: method || 'code',
|
|
96
|
+
lastUsedAt: Date.now()
|
|
97
|
+
}, ...listRecentAccounts().filter(account => account.email !== normalized)].slice(0, MAX_RECENT_ACCOUNTS);
|
|
98
|
+
writeRecentAccounts(next);
|
|
99
|
+
return next;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Removes one account from this browser's list. The account itself, its
|
|
104
|
+
* sessions and the lists of other panels are untouched.
|
|
105
|
+
*
|
|
106
|
+
* @returns the list as it stands after the removal
|
|
107
|
+
*/
|
|
108
|
+
function forgetAccount(email) {
|
|
109
|
+
const normalized = normalizeEmail(email);
|
|
110
|
+
const next = listRecentAccounts().filter(account => account.email !== normalized);
|
|
111
|
+
writeRecentAccounts(next);
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Records the provider this tab is leaving for. */
|
|
116
|
+
function markSocialDeparture(provider) {
|
|
117
|
+
try {
|
|
118
|
+
window.sessionStorage.setItem(SOCIAL_DEPARTURE_KEY, JSON.stringify({
|
|
119
|
+
provider,
|
|
120
|
+
at: Date.now()
|
|
121
|
+
}));
|
|
122
|
+
} catch {
|
|
123
|
+
// No storage: the account is not remembered, and the sign-in still works.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Reads and clears the provider this tab left for, if it left recently.
|
|
129
|
+
*
|
|
130
|
+
* Read BEFORE an identity switch runs: the switch clears `sessionStorage`.
|
|
131
|
+
*/
|
|
132
|
+
function takeSocialDeparture() {
|
|
133
|
+
try {
|
|
134
|
+
const raw = window.sessionStorage.getItem(SOCIAL_DEPARTURE_KEY);
|
|
135
|
+
if (!raw) return null;
|
|
136
|
+
window.sessionStorage.removeItem(SOCIAL_DEPARTURE_KEY);
|
|
137
|
+
const {
|
|
138
|
+
provider,
|
|
139
|
+
at
|
|
140
|
+
} = JSON.parse(raw);
|
|
141
|
+
if (typeof provider !== 'string' || !provider) return null;
|
|
142
|
+
if (!(Date.now() - Number(at) < SOCIAL_DEPARTURE_TTL_MS)) return null;
|
|
143
|
+
return provider;
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
12
149
|
/**
|
|
13
150
|
* The identity switch — when the panel stops being one person and becomes
|
|
14
151
|
* another in the middle of a live session.
|
|
@@ -106,7 +243,12 @@ const KEEP = new Set([
|
|
|
106
243
|
// The switch beacon: a timestamp the sibling tabs listen for. Wiping it
|
|
107
244
|
// would make the NEXT write look unchanged to the browser in some cases,
|
|
108
245
|
// and the event that carries the switch would not fire.
|
|
109
|
-
'auth:identity-switched'
|
|
246
|
+
'auth:identity-switched',
|
|
247
|
+
// The accounts that signed in on this browser (`recent-accounts.js`). It
|
|
248
|
+
// belongs to the browser, not to whoever is signed in, and holds nothing
|
|
249
|
+
// of any account beyond its email — wiping it on every switch would empty
|
|
250
|
+
// the sign-in shortcuts each time an operator impersonates someone.
|
|
251
|
+
RECENT_ACCOUNTS_KEY]);
|
|
110
252
|
function dropStoredAccountState() {
|
|
111
253
|
try {
|
|
112
254
|
const doomed = [];
|
|
@@ -585,14 +727,22 @@ const getSocialProviders = async () => {
|
|
|
585
727
|
* `redirect` is where to come back to; the worker validates it against the
|
|
586
728
|
* application's allowlist BEFORE leaving, and stores the validated value. It
|
|
587
729
|
* defaults to the current page.
|
|
730
|
+
*
|
|
731
|
+
* `rememberAccount: false` keeps the account off this browser's recent list
|
|
732
|
+
* (`recent-accounts.js`) when the token comes back.
|
|
588
733
|
*/
|
|
589
734
|
const startSocialSignIn = (provider, {
|
|
590
|
-
redirect
|
|
735
|
+
redirect,
|
|
736
|
+
rememberAccount: shouldRemember = true
|
|
591
737
|
} = {}) => {
|
|
592
738
|
const destination = redirect || window.location.href.split('#')[0];
|
|
593
739
|
const url = new URL(`${API_BASE}/auth/sign-in/${provider}`);
|
|
594
740
|
url.searchParams.set('redirect', destination);
|
|
595
741
|
if (API_KEY && !INTERNAL_MODE) url.searchParams.set('api_key', API_KEY);
|
|
742
|
+
|
|
743
|
+
// The fragment that comes back carries only the token, not which provider
|
|
744
|
+
// issued it — so the tab notes where it went before leaving.
|
|
745
|
+
if (shouldRemember) markSocialDeparture(provider);
|
|
596
746
|
window.location.assign(url.toString());
|
|
597
747
|
};
|
|
598
748
|
|
|
@@ -611,6 +761,11 @@ const consumeSocialToken = () => {
|
|
|
611
761
|
const token = params.get('token');
|
|
612
762
|
if (!token) return null;
|
|
613
763
|
|
|
764
|
+
// Read before the identity switch below: it clears `sessionStorage`, where
|
|
765
|
+
// the departure is noted. A token without a departure (a handoff between
|
|
766
|
+
// panels) is not a sign-in made here, and does not enter the list.
|
|
767
|
+
const provider = takeSocialDeparture();
|
|
768
|
+
|
|
614
769
|
/*
|
|
615
770
|
* A social sign-in can land on a tab that already belongs to SOMEBODY ELSE.
|
|
616
771
|
*
|
|
@@ -643,6 +798,10 @@ const consumeSocialToken = () => {
|
|
|
643
798
|
if (before && after && before !== after) markIdentitySwitching();
|
|
644
799
|
}
|
|
645
800
|
setStoredToken(token);
|
|
801
|
+
if (provider) {
|
|
802
|
+
const email = decodeJWT(token)?.email;
|
|
803
|
+
if (email) rememberAccount(email, provider);
|
|
804
|
+
}
|
|
646
805
|
params.delete('token');
|
|
647
806
|
const rest = params.toString();
|
|
648
807
|
window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}${rest ? `#${rest}` : ''}`);
|
|
@@ -661,6 +820,9 @@ const consumeSocialError = () => {
|
|
|
661
820
|
const params = new URLSearchParams(window.location.search);
|
|
662
821
|
const reason = params.get('social_error');
|
|
663
822
|
if (!reason) return null;
|
|
823
|
+
|
|
824
|
+
// The trip failed: the departure noted for it must not label a later token.
|
|
825
|
+
takeSocialDeparture();
|
|
664
826
|
params.delete('social_error');
|
|
665
827
|
const rest = params.toString();
|
|
666
828
|
window.history.replaceState(null, '', `${window.location.pathname}${rest ? `?${rest}` : ''}${window.location.hash}`);
|
|
@@ -2029,6 +2191,156 @@ function AuthCard({
|
|
|
2029
2191
|
});
|
|
2030
2192
|
}
|
|
2031
2193
|
|
|
2194
|
+
const PROVIDER_NAMES = {
|
|
2195
|
+
google: 'Google',
|
|
2196
|
+
github: 'GitHub'
|
|
2197
|
+
};
|
|
2198
|
+
const providerName = method => PROVIDER_NAMES[method] || method.charAt(0).toUpperCase() + method.slice(1);
|
|
2199
|
+
const RELATIVE = new Intl.RelativeTimeFormat('pt-BR', {
|
|
2200
|
+
numeric: 'auto'
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
/** "há 2 horas", "ontem", "há 6 dias" — the unit that reads naturally. */
|
|
2204
|
+
function formatLastUsed(timestamp, now = Date.now()) {
|
|
2205
|
+
const minutes = Math.round((timestamp - now) / 60000);
|
|
2206
|
+
if (Math.abs(minutes) < 1) return RELATIVE.format(0, 'second');
|
|
2207
|
+
if (Math.abs(minutes) < 60) return RELATIVE.format(minutes, 'minute');
|
|
2208
|
+
const hours = Math.round(minutes / 60);
|
|
2209
|
+
if (Math.abs(hours) < 24) return RELATIVE.format(hours, 'hour');
|
|
2210
|
+
const days = Math.round(hours / 24);
|
|
2211
|
+
if (Math.abs(days) < 30) return RELATIVE.format(days, 'day');
|
|
2212
|
+
const months = Math.round(days / 30);
|
|
2213
|
+
if (Math.abs(months) < 12) return RELATIVE.format(months, 'month');
|
|
2214
|
+
return RELATIVE.format(Math.round(days / 365), 'year');
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
/** "ciro" → "C", "qa+monitors" → "QM". */
|
|
2218
|
+
function initialsOf(email) {
|
|
2219
|
+
const local = email.split('@')[0];
|
|
2220
|
+
const parts = local.split(/[.+_-]/).filter(Boolean);
|
|
2221
|
+
return ((parts[0] || local).charAt(0) + (parts[1] ? parts[1].charAt(0) : '')).toUpperCase();
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
/**
|
|
2225
|
+
* The first step for someone this browser already knows.
|
|
2226
|
+
*
|
|
2227
|
+
* Each row is the whole action: clicking it sends the code (or leaves for the
|
|
2228
|
+
* provider the account used last time), so a returning person goes from
|
|
2229
|
+
* opening the screen to typing the code in one click.
|
|
2230
|
+
*
|
|
2231
|
+
* Removing is behind "Gerenciar" on purpose. An × always in view sits right
|
|
2232
|
+
* next to the row the person came to click, and a slip would drop the account
|
|
2233
|
+
* they meant to use. Nothing is lost by removing — signing in again brings the
|
|
2234
|
+
* account back — so there is no confirmation step either.
|
|
2235
|
+
*/
|
|
2236
|
+
function RecentAccounts({
|
|
2237
|
+
accounts,
|
|
2238
|
+
pickingEmail = null,
|
|
2239
|
+
managing = false,
|
|
2240
|
+
onToggleManage,
|
|
2241
|
+
onPick,
|
|
2242
|
+
onForget,
|
|
2243
|
+
onUseOther,
|
|
2244
|
+
labels = {}
|
|
2245
|
+
}) {
|
|
2246
|
+
const busy = !!pickingEmail;
|
|
2247
|
+
return /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
|
|
2248
|
+
gap: "md",
|
|
2249
|
+
children: [/*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
|
|
2250
|
+
gap: 8,
|
|
2251
|
+
children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
|
|
2252
|
+
justify: "space-between",
|
|
2253
|
+
align: "baseline",
|
|
2254
|
+
wrap: "nowrap",
|
|
2255
|
+
children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
|
|
2256
|
+
fz: 11,
|
|
2257
|
+
fw: 800,
|
|
2258
|
+
lh: 1,
|
|
2259
|
+
tt: "uppercase",
|
|
2260
|
+
lts: "1.5px",
|
|
2261
|
+
c: "gray.4",
|
|
2262
|
+
children: labels.recentAccountsHeading || 'Contas neste navegador'
|
|
2263
|
+
}), /*#__PURE__*/jsxRuntime.jsx(core.Anchor, {
|
|
2264
|
+
component: "button",
|
|
2265
|
+
type: "button"
|
|
2266
|
+
/*
|
|
2267
|
+
* The size of the heading it sits beside, not of body
|
|
2268
|
+
* text: it is a secondary control of the list, and at
|
|
2269
|
+
* 14px it outweighed the 11px label it annotates.
|
|
2270
|
+
*/,
|
|
2271
|
+
fz: 12,
|
|
2272
|
+
lh: 1,
|
|
2273
|
+
c: "dimmed",
|
|
2274
|
+
onClick: busy ? undefined : onToggleManage,
|
|
2275
|
+
children: managing ? labels.recentAccountsDone || 'Concluir' : labels.recentAccountsManage || 'Gerenciar'
|
|
2276
|
+
})]
|
|
2277
|
+
}), /*#__PURE__*/jsxRuntime.jsx(core.Paper, {
|
|
2278
|
+
withBorder: true,
|
|
2279
|
+
radius: 0,
|
|
2280
|
+
p: 0,
|
|
2281
|
+
children: accounts.map((account, index) => {
|
|
2282
|
+
const isPicking = pickingEmail === account.email;
|
|
2283
|
+
const isSocial = account.method !== 'code';
|
|
2284
|
+
const description = isPicking ? isSocial ? `${labels.openingProvider || 'Abrindo o'} ${providerName(account.method)}…` : labels.sendingCode || 'Enviando código…' : `${labels.lastUsed || 'Último acesso'} ${formatLastUsed(account.lastUsedAt)}${isSocial ? ` · ${providerName(account.method)}` : ''}`;
|
|
2285
|
+
return /*#__PURE__*/jsxRuntime.jsx(core.NavLink, {
|
|
2286
|
+
/*
|
|
2287
|
+
* A `div` while managing: the row then holds the
|
|
2288
|
+
* remove button, and a button inside a button is
|
|
2289
|
+
* invalid HTML that browsers repair by splitting
|
|
2290
|
+
* the row in two.
|
|
2291
|
+
*/
|
|
2292
|
+
component: managing ? 'div' : 'button',
|
|
2293
|
+
type: managing ? undefined : 'button',
|
|
2294
|
+
"aria-disabled": busy || undefined,
|
|
2295
|
+
onClick: managing || busy ? undefined : () => onPick(account),
|
|
2296
|
+
noWrap: true,
|
|
2297
|
+
label: /*#__PURE__*/jsxRuntime.jsx(core.Text, {
|
|
2298
|
+
fz: 14,
|
|
2299
|
+
fw: 600,
|
|
2300
|
+
c: "gray.9",
|
|
2301
|
+
truncate: true,
|
|
2302
|
+
children: account.email
|
|
2303
|
+
}),
|
|
2304
|
+
description: description,
|
|
2305
|
+
leftSection: /*#__PURE__*/jsxRuntime.jsx(core.Avatar, {
|
|
2306
|
+
radius: 0,
|
|
2307
|
+
size: 36,
|
|
2308
|
+
color: "gray",
|
|
2309
|
+
variant: "light",
|
|
2310
|
+
children: initialsOf(account.email)
|
|
2311
|
+
}),
|
|
2312
|
+
rightSection: managing ? /*#__PURE__*/jsxRuntime.jsx(core.ActionIcon, {
|
|
2313
|
+
variant: "subtle",
|
|
2314
|
+
color: "gray",
|
|
2315
|
+
"aria-label": `${labels.removeAccount || 'Remover'} ${account.email}`,
|
|
2316
|
+
onClick: () => onForget(account),
|
|
2317
|
+
children: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconX, {
|
|
2318
|
+
size: 16
|
|
2319
|
+
})
|
|
2320
|
+
}) : isPicking ? /*#__PURE__*/jsxRuntime.jsx(core.Loader, {
|
|
2321
|
+
size: 14
|
|
2322
|
+
}) : /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconArrowRight, {
|
|
2323
|
+
size: 16,
|
|
2324
|
+
color: "var(--mantine-color-gray-5)"
|
|
2325
|
+
}),
|
|
2326
|
+
py: 10,
|
|
2327
|
+
style: index > 0 ? {
|
|
2328
|
+
borderTop: '1px solid var(--mantine-color-gray-2)'
|
|
2329
|
+
} : undefined
|
|
2330
|
+
}, account.email);
|
|
2331
|
+
})
|
|
2332
|
+
})]
|
|
2333
|
+
}), /*#__PURE__*/jsxRuntime.jsx(core.Button, {
|
|
2334
|
+
type: "button",
|
|
2335
|
+
variant: "default",
|
|
2336
|
+
fullWidth: true,
|
|
2337
|
+
"aria-disabled": busy,
|
|
2338
|
+
onClick: busy ? undefined : onUseOther,
|
|
2339
|
+
children: labels.useOtherEmail || 'Usar outro e-mail'
|
|
2340
|
+
})]
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2032
2344
|
const MARKS = {
|
|
2033
2345
|
google: iconsReact.IconBrandGoogle
|
|
2034
2346
|
};
|
|
@@ -2050,7 +2362,11 @@ const MARKS = {
|
|
|
2050
2362
|
function SocialButtons({
|
|
2051
2363
|
labels = {},
|
|
2052
2364
|
redirect,
|
|
2053
|
-
disabled = false
|
|
2365
|
+
disabled = false,
|
|
2366
|
+
// Whether the account returning from the provider joins this browser's
|
|
2367
|
+
// recent list (`recent-accounts.js`). `<SignIn recentAccounts={false}>`
|
|
2368
|
+
// turns it off here too.
|
|
2369
|
+
rememberAccount = true
|
|
2054
2370
|
}) {
|
|
2055
2371
|
const [providers, setProviders] = react.useState(null);
|
|
2056
2372
|
const [leaving, setLeaving] = react.useState(null);
|
|
@@ -2094,7 +2410,8 @@ function SocialButtons({
|
|
|
2094
2410
|
// `oauth_states` row.
|
|
2095
2411
|
setLeaving(provider.provider);
|
|
2096
2412
|
startSocialSignIn(provider.provider, {
|
|
2097
|
-
redirect
|
|
2413
|
+
redirect,
|
|
2414
|
+
rememberAccount
|
|
2098
2415
|
});
|
|
2099
2416
|
},
|
|
2100
2417
|
children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
|
|
@@ -2203,6 +2520,102 @@ function TermsNotice({
|
|
|
2203
2520
|
});
|
|
2204
2521
|
}
|
|
2205
2522
|
|
|
2523
|
+
/**
|
|
2524
|
+
* What a failed `verify` means for the person, read from the worker's answer.
|
|
2525
|
+
*
|
|
2526
|
+
* The HTTP status carries the case — `code` is `VALIDATION_ERROR` for both a
|
|
2527
|
+
* wrong and an expired code, so it cannot tell them apart:
|
|
2528
|
+
* - 400 with `details.attemptsLeft`: wrong code, the request is still open;
|
|
2529
|
+
* - 400 without it: no open request for this email (already replaced);
|
|
2530
|
+
* - 429: the fifth wrong try destroyed the request;
|
|
2531
|
+
* - 410: the code outlived its minutes, and was destroyed too.
|
|
2532
|
+
*
|
|
2533
|
+
* Anything else — the network, a 500 — is not about the code, and must not
|
|
2534
|
+
* lock the field.
|
|
2535
|
+
*/
|
|
2536
|
+
function describeCodeFailure(error) {
|
|
2537
|
+
if (error?.status === 429) return {
|
|
2538
|
+
kind: 'exhausted',
|
|
2539
|
+
isLocked: true
|
|
2540
|
+
};
|
|
2541
|
+
if (error?.status === 410) return {
|
|
2542
|
+
kind: 'expired',
|
|
2543
|
+
isLocked: true
|
|
2544
|
+
};
|
|
2545
|
+
if (error?.status === 400) {
|
|
2546
|
+
const attemptsLeft = error?.details?.attemptsLeft;
|
|
2547
|
+
return {
|
|
2548
|
+
kind: 'wrong',
|
|
2549
|
+
isLocked: false,
|
|
2550
|
+
attemptsLeft: Number.isInteger(attemptsLeft) ? attemptsLeft : null
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
return {
|
|
2554
|
+
kind: 'other',
|
|
2555
|
+
isLocked: false,
|
|
2556
|
+
message: error?.message || null
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
/**
|
|
2561
|
+
* The notice above the code field.
|
|
2562
|
+
*
|
|
2563
|
+
* It sits ABOVE the field, not under it, and says what to do next — not only
|
|
2564
|
+
* that something failed. The field's own error line was 12px of red under a
|
|
2565
|
+
* cleared input, next to a greyed-out button: it read as a frozen screen.
|
|
2566
|
+
*
|
|
2567
|
+
* The most common cause gets named: a new code invalidates the previous one,
|
|
2568
|
+
* and the person is often reading an older email.
|
|
2569
|
+
*/
|
|
2570
|
+
function CodeFailureNotice({
|
|
2571
|
+
failure,
|
|
2572
|
+
labels
|
|
2573
|
+
}) {
|
|
2574
|
+
if (!failure) return null;
|
|
2575
|
+
const texts = {
|
|
2576
|
+
wrong: {
|
|
2577
|
+
title: labels.wrongCodeTitle || 'Código incorreto',
|
|
2578
|
+
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(' ')
|
|
2579
|
+
},
|
|
2580
|
+
exhausted: {
|
|
2581
|
+
title: labels.attemptsExhaustedTitle || 'Tentativas esgotadas',
|
|
2582
|
+
body: labels.attemptsExhausted || 'Por segurança, este código foi cancelado. Peça um novo para continuar.'
|
|
2583
|
+
},
|
|
2584
|
+
expired: {
|
|
2585
|
+
title: labels.codeExpiredTitle || 'Código expirado',
|
|
2586
|
+
body: labels.codeExpired || 'O código vale por poucos minutos. Peça um novo para continuar.'
|
|
2587
|
+
},
|
|
2588
|
+
other: {
|
|
2589
|
+
title: labels.codeFailedTitle || 'Não foi possível entrar',
|
|
2590
|
+
body: failure.message || labels.invalidCode || 'Tente de novo em instantes.'
|
|
2591
|
+
}
|
|
2592
|
+
}[failure.kind];
|
|
2593
|
+
return /*#__PURE__*/jsxRuntime.jsx(core.Alert, {
|
|
2594
|
+
color: "red",
|
|
2595
|
+
variant: "light",
|
|
2596
|
+
radius: 0,
|
|
2597
|
+
icon: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconAlertCircle, {
|
|
2598
|
+
size: 18
|
|
2599
|
+
}),
|
|
2600
|
+
title: texts.title
|
|
2601
|
+
/*
|
|
2602
|
+
* `role="alert"` is Mantine's default and is what makes a screen
|
|
2603
|
+
* reader announce the failure without the person moving focus away
|
|
2604
|
+
* from the field they are about to retype in.
|
|
2605
|
+
*/,
|
|
2606
|
+
styles: {
|
|
2607
|
+
root: {
|
|
2608
|
+
border: '1px solid var(--mantine-color-red-2)'
|
|
2609
|
+
}
|
|
2610
|
+
},
|
|
2611
|
+
children: /*#__PURE__*/jsxRuntime.jsx(core.Text, {
|
|
2612
|
+
size: "sm",
|
|
2613
|
+
lh: 1.45,
|
|
2614
|
+
children: texts.body
|
|
2615
|
+
})
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2206
2619
|
// The OAuth flow's pass-through screen.
|
|
2207
2620
|
//
|
|
2208
2621
|
// The panel is not the destination here: the user is authorizing an MCP client
|
|
@@ -2293,6 +2706,15 @@ function SignIn({
|
|
|
2293
2706
|
* `false` opts out entirely, for a screen that wants the emailed code only.
|
|
2294
2707
|
*/
|
|
2295
2708
|
socialLogin = 'auto',
|
|
2709
|
+
/*
|
|
2710
|
+
* The accounts that already signed in on this browser, offered before the
|
|
2711
|
+
* empty field (`recent-accounts.js`). With none saved the screen is exactly
|
|
2712
|
+
* the email form, so the default changes nothing for a first visit.
|
|
2713
|
+
*
|
|
2714
|
+
* `false` neither shows nor saves: a shared computer, or an internal panel,
|
|
2715
|
+
* should not remember who used it.
|
|
2716
|
+
*/
|
|
2717
|
+
recentAccounts = true,
|
|
2296
2718
|
...cardProps
|
|
2297
2719
|
}) {
|
|
2298
2720
|
const user = useAuthStore(s => s.user);
|
|
@@ -2306,7 +2728,18 @@ function SignIn({
|
|
|
2306
2728
|
// present it again: it is the (email, code) pair the server validates.
|
|
2307
2729
|
const [sentTo, setSentTo] = react.useState(null);
|
|
2308
2730
|
const [code, setCode] = react.useState('');
|
|
2309
|
-
const [
|
|
2731
|
+
const [codeFailure, setCodeFailure] = react.useState(null);
|
|
2732
|
+
const [isCodeResent, setIsCodeResent] = react.useState(false);
|
|
2733
|
+
const codeInputRef = react.useRef(null);
|
|
2734
|
+
|
|
2735
|
+
// Read once, on mount: the list only changes through this screen, and
|
|
2736
|
+
// every change below writes the new list back into state.
|
|
2737
|
+
const [accounts, setAccounts] = react.useState(() => recentAccounts ? listRecentAccounts() : []);
|
|
2738
|
+
const [isChoosingOther, setIsChoosingOther] = react.useState(false);
|
|
2739
|
+
const [isManaging, setIsManaging] = react.useState(false);
|
|
2740
|
+
const [pickingEmail, setPickingEmail] = react.useState(null);
|
|
2741
|
+
const isShowingAccounts = recentAccounts && accounts.length > 0 && !isChoosingOther;
|
|
2742
|
+
const isCodeLocked = !!codeFailure?.isLocked;
|
|
2310
2743
|
|
|
2311
2744
|
// Hook that fetches the application's logo
|
|
2312
2745
|
const applicationLogo = useApplicationLogo();
|
|
@@ -2353,18 +2786,66 @@ function SignIn({
|
|
|
2353
2786
|
|
|
2354
2787
|
// Step 1 — ask for the code.
|
|
2355
2788
|
const handleRequest = async values => {
|
|
2356
|
-
if (sending) return;
|
|
2789
|
+
if (sending) return false;
|
|
2357
2790
|
try {
|
|
2358
2791
|
await requestCode(values.email);
|
|
2359
2792
|
setSentTo(values.email);
|
|
2360
2793
|
setCode('');
|
|
2361
|
-
|
|
2794
|
+
setCodeFailure(null);
|
|
2362
2795
|
onCodeSent?.(values.email);
|
|
2796
|
+
return true;
|
|
2363
2797
|
} catch (error) {
|
|
2364
|
-
|
|
2798
|
+
// Nothing on the card shows this one: the app's notification is
|
|
2799
|
+
// the only place the person learns the code was not sent.
|
|
2800
|
+
onError?.(error, {
|
|
2801
|
+
step: 'request',
|
|
2802
|
+
isShownOnCard: false
|
|
2803
|
+
});
|
|
2804
|
+
return false;
|
|
2365
2805
|
}
|
|
2366
2806
|
};
|
|
2367
2807
|
|
|
2808
|
+
// A new code, from the code step. The worker replaces the request, so the
|
|
2809
|
+
// attempts start over and the previous code stops working — the notice
|
|
2810
|
+
// says so, or the person keeps typing the one from the older email.
|
|
2811
|
+
const handleResend = async () => {
|
|
2812
|
+
const isSent = await handleRequest({
|
|
2813
|
+
email: sentTo
|
|
2814
|
+
});
|
|
2815
|
+
setIsCodeResent(isSent);
|
|
2816
|
+
if (isSent) codeInputRef.current?.focus();
|
|
2817
|
+
};
|
|
2818
|
+
|
|
2819
|
+
// Step 1, from the list — the click on a saved account IS the request.
|
|
2820
|
+
//
|
|
2821
|
+
// An account that came in through a provider goes back to that provider,
|
|
2822
|
+
// as long as the application still offers it. If the owner turned it off
|
|
2823
|
+
// in the meantime the emailed code still works for the same email, so that
|
|
2824
|
+
// is the fallback rather than a dead row.
|
|
2825
|
+
const handlePick = async account => {
|
|
2826
|
+
if (pickingEmail || sending) return;
|
|
2827
|
+
setPickingEmail(account.email);
|
|
2828
|
+
if (account.method !== 'code' && socialLogin !== false) {
|
|
2829
|
+
const providers = await getSocialProviders();
|
|
2830
|
+
if (providers?.some(provider => provider.provider === account.method)) {
|
|
2831
|
+
// The page is leaving; the row keeps its spinner until it does.
|
|
2832
|
+
startSocialSignIn(account.method, {
|
|
2833
|
+
rememberAccount: recentAccounts
|
|
2834
|
+
});
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
await handleRequest({
|
|
2839
|
+
email: account.email
|
|
2840
|
+
});
|
|
2841
|
+
setPickingEmail(null);
|
|
2842
|
+
};
|
|
2843
|
+
const handleForget = account => {
|
|
2844
|
+
const next = forgetAccount(account.email);
|
|
2845
|
+
setAccounts(next);
|
|
2846
|
+
if (next.length === 0) setIsManaging(false);
|
|
2847
|
+
};
|
|
2848
|
+
|
|
2368
2849
|
// Step 2 — trade the code for the session.
|
|
2369
2850
|
//
|
|
2370
2851
|
// The redirect is decided AND EXECUTED before onSuccess.
|
|
@@ -2382,9 +2863,15 @@ function SignIn({
|
|
|
2382
2863
|
// signals that navigation was taken over — now as information, not as a
|
|
2383
2864
|
// trap.
|
|
2384
2865
|
const handleVerify = async value => {
|
|
2385
|
-
|
|
2866
|
+
setCodeFailure(null);
|
|
2867
|
+
setIsCodeResent(false);
|
|
2386
2868
|
try {
|
|
2387
2869
|
const result = await verifyCode(sentTo, value);
|
|
2870
|
+
|
|
2871
|
+
// Only now, with a session: an email that never received a valid
|
|
2872
|
+
// code never becomes a suggestion. Written before the redirect,
|
|
2873
|
+
// which may unmount this screen.
|
|
2874
|
+
if (recentAccounts) setAccounts(rememberAccount(sentTo, 'code'));
|
|
2388
2875
|
const target = handleRedirect ? getRedirectFromLocation(redirectOrigins) : null;
|
|
2389
2876
|
if (target) applyRedirect(target, navigate);
|
|
2390
2877
|
onSuccess?.(result?.user ?? null, {
|
|
@@ -2392,12 +2879,19 @@ function SignIn({
|
|
|
2392
2879
|
redirectHandled: !!target
|
|
2393
2880
|
});
|
|
2394
2881
|
} catch (error) {
|
|
2395
|
-
// The
|
|
2396
|
-
//
|
|
2397
|
-
//
|
|
2398
|
-
|
|
2882
|
+
// The failure belongs to this card, not to the global notification:
|
|
2883
|
+
// the person is looking at the eight characters they just typed.
|
|
2884
|
+
// The field goes back empty and focused, ready for the next try.
|
|
2885
|
+
setCodeFailure(describeCodeFailure(error));
|
|
2399
2886
|
setCode('');
|
|
2400
|
-
|
|
2887
|
+
codeInputRef.current?.focus();
|
|
2888
|
+
// Still reported — an app may log it — but flagged: the card
|
|
2889
|
+
// already explains it, and a notification repeating "Código
|
|
2890
|
+
// inválido" in the corner would say it twice.
|
|
2891
|
+
onError?.(error, {
|
|
2892
|
+
step: 'verify',
|
|
2893
|
+
isShownOnCard: true
|
|
2894
|
+
});
|
|
2401
2895
|
}
|
|
2402
2896
|
};
|
|
2403
2897
|
|
|
@@ -2419,17 +2913,43 @@ function SignIn({
|
|
|
2419
2913
|
logo: finalLogo,
|
|
2420
2914
|
logoWidth: logoWidth,
|
|
2421
2915
|
title: title,
|
|
2422
|
-
subtitle: sentTo ? labels.codeSent || 'Digite o código que enviamos' : subtitle,
|
|
2916
|
+
subtitle: sentTo ? labels.codeSent || 'Digite o código que enviamos' : isShowingAccounts ? labels.recentAccountsSubtitle || 'Escolha a conta que vai receber o código' : subtitle,
|
|
2423
2917
|
variant: variant,
|
|
2424
2918
|
opened: opened,
|
|
2425
2919
|
onClose: onClose,
|
|
2426
2920
|
modalProps: modalProps,
|
|
2427
2921
|
...cardProps,
|
|
2428
|
-
children: !sentTo ? /*#__PURE__*/jsxRuntime.jsx(
|
|
2922
|
+
children: !sentTo && isShowingAccounts ? /*#__PURE__*/jsxRuntime.jsx(RecentAccounts, {
|
|
2923
|
+
accounts: accounts,
|
|
2924
|
+
pickingEmail: pickingEmail,
|
|
2925
|
+
managing: isManaging,
|
|
2926
|
+
onToggleManage: () => setIsManaging(value => !value),
|
|
2927
|
+
onPick: handlePick,
|
|
2928
|
+
onForget: handleForget,
|
|
2929
|
+
onUseOther: () => {
|
|
2930
|
+
setIsManaging(false);
|
|
2931
|
+
setIsChoosingOther(true);
|
|
2932
|
+
},
|
|
2933
|
+
labels: labels
|
|
2934
|
+
}) : !sentTo ? /*#__PURE__*/jsxRuntime.jsx("form", {
|
|
2429
2935
|
onSubmit: form$1.onSubmit(handleRequest),
|
|
2430
2936
|
children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
|
|
2431
2937
|
gap: "md",
|
|
2432
|
-
children: [/*#__PURE__*/jsxRuntime.jsx(core.
|
|
2938
|
+
children: [recentAccounts && accounts.length > 0 && /*#__PURE__*/jsxRuntime.jsx(core.Anchor, {
|
|
2939
|
+
component: "button",
|
|
2940
|
+
type: "button",
|
|
2941
|
+
size: "sm",
|
|
2942
|
+
c: "dimmed",
|
|
2943
|
+
w: "fit-content",
|
|
2944
|
+
onClick: () => setIsChoosingOther(false),
|
|
2945
|
+
children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
|
|
2946
|
+
gap: 6,
|
|
2947
|
+
wrap: "nowrap",
|
|
2948
|
+
children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconArrowLeft, {
|
|
2949
|
+
size: 14
|
|
2950
|
+
}), `${labels.savedAccounts || 'Contas salvas'} (${accounts.length})`]
|
|
2951
|
+
})
|
|
2952
|
+
}), /*#__PURE__*/jsxRuntime.jsx(core.TextInput, {
|
|
2433
2953
|
label: labels.email || 'Email',
|
|
2434
2954
|
placeholder: labels.emailPlaceholder || 'seu@email.com',
|
|
2435
2955
|
type: "email",
|
|
@@ -2471,7 +2991,8 @@ function SignIn({
|
|
|
2471
2991
|
children: sending ? labels.sendingCode || 'Enviando…' : labels.sendCodeButton || 'Enviar código'
|
|
2472
2992
|
}), socialLogin !== false && /*#__PURE__*/jsxRuntime.jsx(SocialButtons, {
|
|
2473
2993
|
labels: labels,
|
|
2474
|
-
disabled: sending
|
|
2994
|
+
disabled: sending,
|
|
2995
|
+
rememberAccount: recentAccounts
|
|
2475
2996
|
}), /*#__PURE__*/jsxRuntime.jsx(TermsNotice, {
|
|
2476
2997
|
url: termsUrl,
|
|
2477
2998
|
text: labels.termsNotice || 'Criando uma conta, você concorda com todos os nossos',
|
|
@@ -2480,7 +3001,27 @@ function SignIn({
|
|
|
2480
3001
|
})
|
|
2481
3002
|
}) : /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
|
|
2482
3003
|
gap: "md",
|
|
2483
|
-
children: [/*#__PURE__*/jsxRuntime.jsx(core.
|
|
3004
|
+
children: [isCodeResent && /*#__PURE__*/jsxRuntime.jsx(core.Alert, {
|
|
3005
|
+
color: "gray",
|
|
3006
|
+
variant: "light",
|
|
3007
|
+
radius: 0,
|
|
3008
|
+
p: "xs",
|
|
3009
|
+
children: /*#__PURE__*/jsxRuntime.jsxs(core.Text, {
|
|
3010
|
+
size: "xs",
|
|
3011
|
+
lh: 1.4,
|
|
3012
|
+
children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
|
|
3013
|
+
span: true,
|
|
3014
|
+
inherit: true,
|
|
3015
|
+
fw: 700,
|
|
3016
|
+
c: "gray.9",
|
|
3017
|
+
children: labels.codeResentTitle || 'Novo código enviado.'
|
|
3018
|
+
}), ' ', labels.codeResent || 'O anterior deixou de valer.']
|
|
3019
|
+
})
|
|
3020
|
+
}), /*#__PURE__*/jsxRuntime.jsx(CodeFailureNotice, {
|
|
3021
|
+
failure: codeFailure,
|
|
3022
|
+
labels: labels
|
|
3023
|
+
}), /*#__PURE__*/jsxRuntime.jsx(core.TextInput, {
|
|
3024
|
+
ref: codeInputRef,
|
|
2484
3025
|
label: labels.codeLabel || 'Código de acesso'
|
|
2485
3026
|
/*
|
|
2486
3027
|
* The email is the field's description, not the subtitle:
|
|
@@ -2500,26 +3041,56 @@ function SignIn({
|
|
|
2500
3041
|
value: code,
|
|
2501
3042
|
onChange: event => {
|
|
2502
3043
|
setCode(event.currentTarget.value);
|
|
2503
|
-
|
|
3044
|
+
// Typing again is the correction: the notice has
|
|
3045
|
+
// done its job. A locked field cannot be typed in,
|
|
3046
|
+
// so an exhausted or expired notice stays.
|
|
3047
|
+
if (codeFailure) setCodeFailure(null);
|
|
2504
3048
|
},
|
|
2505
3049
|
onKeyDown: event => {
|
|
2506
3050
|
if (event.key === 'Enter' && code.trim()) handleVerify(code);
|
|
2507
3051
|
},
|
|
2508
3052
|
autoFocus: true,
|
|
2509
3053
|
autoComplete: "one-time-code",
|
|
2510
|
-
readOnly: verifying
|
|
2511
|
-
|
|
2512
|
-
|
|
3054
|
+
readOnly: verifying
|
|
3055
|
+
/*
|
|
3056
|
+
* No `error` on the field: the notice above carries the
|
|
3057
|
+
* failure, and a red border around an empty field turned
|
|
3058
|
+
* the PLACEHOLDER red — "ABCD-EFGH" read as the code the
|
|
3059
|
+
* person had typed. With the request gone there is
|
|
3060
|
+
* nothing left to type into.
|
|
3061
|
+
*/,
|
|
3062
|
+
disabled: isCodeLocked
|
|
3063
|
+
}), isCodeLocked ?
|
|
3064
|
+
/*#__PURE__*/
|
|
3065
|
+
/*
|
|
3066
|
+
* The request is gone: confirming can only fail again, so
|
|
3067
|
+
* the one action that works takes the button's place.
|
|
3068
|
+
*/
|
|
3069
|
+
jsxRuntime.jsx(core.Button, {
|
|
3070
|
+
type: "button",
|
|
3071
|
+
fullWidth: true,
|
|
3072
|
+
"aria-disabled": sending,
|
|
3073
|
+
onClick: sending ? undefined : handleResend,
|
|
3074
|
+
leftSection: sending ? /*#__PURE__*/jsxRuntime.jsx(core.Loader, {
|
|
3075
|
+
size: 14,
|
|
3076
|
+
color: "gray.0"
|
|
3077
|
+
}) : /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconRefresh, {
|
|
3078
|
+
size: 16
|
|
3079
|
+
}),
|
|
3080
|
+
children: sending ? labels.sendingCode || 'Enviando…' : labels.sendNewCode || 'Enviar novo código'
|
|
3081
|
+
}) : /*#__PURE__*/jsxRuntime.jsx(core.Button, {
|
|
2513
3082
|
type: "button",
|
|
2514
3083
|
fullWidth: true
|
|
2515
|
-
// Same reason as the previous step: `disabled` would
|
|
2516
|
-
// the button exactly while signing in happens.
|
|
2517
|
-
//
|
|
2518
|
-
//
|
|
3084
|
+
// Same reason as the previous step: `disabled` would
|
|
3085
|
+
// fade the button exactly while signing in happens.
|
|
3086
|
+
//
|
|
3087
|
+
// Never disabled for an empty field either. Right
|
|
3088
|
+
// after a failure the field is empty on purpose, and
|
|
3089
|
+
// a grey button there read as a frozen screen; the
|
|
3090
|
+
// click sends the cursor to the field instead.
|
|
2519
3091
|
,
|
|
2520
3092
|
"aria-disabled": verifying,
|
|
2521
|
-
|
|
2522
|
-
onClick: verifying ? undefined : () => handleVerify(code),
|
|
3093
|
+
onClick: verifying ? undefined : () => code.trim() ? handleVerify(code) : codeInputRef.current?.focus(),
|
|
2523
3094
|
leftSection: verifying ? /*#__PURE__*/jsxRuntime.jsx(core.Loader, {
|
|
2524
3095
|
size: 14,
|
|
2525
3096
|
color: "gray.0"
|
|
@@ -2537,15 +3108,17 @@ function SignIn({
|
|
|
2537
3108
|
onClick: () => {
|
|
2538
3109
|
setSentTo(null);
|
|
2539
3110
|
setCode('');
|
|
2540
|
-
|
|
3111
|
+
setCodeFailure(null);
|
|
3112
|
+
setIsCodeResent(false);
|
|
3113
|
+
// The label promises another email: the form,
|
|
3114
|
+
// not the list the person may have come from.
|
|
3115
|
+
setIsChoosingOther(true);
|
|
2541
3116
|
},
|
|
2542
3117
|
children: labels.changeEmail || 'Usar outro e-mail'
|
|
2543
|
-
}), /*#__PURE__*/jsxRuntime.jsx(core.Anchor, {
|
|
3118
|
+
}), !isCodeLocked && /*#__PURE__*/jsxRuntime.jsx(core.Anchor, {
|
|
2544
3119
|
size: "sm",
|
|
2545
3120
|
c: "dimmed",
|
|
2546
|
-
onClick: sending ? undefined :
|
|
2547
|
-
email: sentTo
|
|
2548
|
-
}),
|
|
3121
|
+
onClick: sending ? undefined : handleResend,
|
|
2549
3122
|
children: sending ? labels.sendingCode || 'Enviando…' : labels.resendCode || 'Reenviar código'
|
|
2550
3123
|
})]
|
|
2551
3124
|
})]
|
|
@@ -3535,7 +4108,9 @@ exports.AuthLoading = AuthLoading;
|
|
|
3535
4108
|
exports.AuthProvider = AuthProvider;
|
|
3536
4109
|
exports.GuestOnly = GuestOnly;
|
|
3537
4110
|
exports.IDENTITY_CHANGED_EVENT = IDENTITY_CHANGED_EVENT;
|
|
4111
|
+
exports.MAX_RECENT_ACCOUNTS = MAX_RECENT_ACCOUNTS;
|
|
3538
4112
|
exports.Protect = Protect;
|
|
4113
|
+
exports.RECENT_ACCOUNTS_KEY = RECENT_ACCOUNTS_KEY;
|
|
3539
4114
|
exports.SignIn = SignIn;
|
|
3540
4115
|
exports.SignInButton = SignInButton;
|
|
3541
4116
|
exports.SignOutButton = SignOutButton;
|
|
@@ -3554,6 +4129,7 @@ exports.consumeSocialError = consumeSocialError;
|
|
|
3554
4129
|
exports.consumeSocialToken = consumeSocialToken;
|
|
3555
4130
|
exports.decodeJWT = decodeJWT;
|
|
3556
4131
|
exports.endImpersonation = endImpersonation;
|
|
4132
|
+
exports.forgetAccount = forgetAccount;
|
|
3557
4133
|
exports.getApiUrl = getApiUrl;
|
|
3558
4134
|
exports.getApplicationInfo = getApplicationInfo;
|
|
3559
4135
|
exports.getCurrentUser = getCurrentUser;
|
|
@@ -3564,10 +4140,12 @@ exports.getSocialProviders = getSocialProviders;
|
|
|
3564
4140
|
exports.isAuthenticated = isAuthenticated;
|
|
3565
4141
|
exports.isIdentitySwitching = isIdentitySwitching;
|
|
3566
4142
|
exports.isInternal = isInternal;
|
|
4143
|
+
exports.listRecentAccounts = listRecentAccounts;
|
|
3567
4144
|
exports.listSessions = listSessions;
|
|
3568
4145
|
exports.markIdentitySwitching = markIdentitySwitching;
|
|
3569
4146
|
exports.pollCode = pollCode;
|
|
3570
4147
|
exports.refreshToken = refreshToken;
|
|
4148
|
+
exports.rememberAccount = rememberAccount;
|
|
3571
4149
|
exports.requestCode = requestCode;
|
|
3572
4150
|
exports.resolveRedirect = resolveRedirect;
|
|
3573
4151
|
exports.revokeOtherSessions = revokeOtherSessions;
|