@ciromaciel/auth-react 1.1.0 → 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 +8 -1
- package/dist/index.esm.js +203 -29
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +200 -26
- 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
|
|
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.
|
|
@@ -2518,6 +2518,102 @@ function TermsNotice({
|
|
|
2518
2518
|
});
|
|
2519
2519
|
}
|
|
2520
2520
|
|
|
2521
|
+
/**
|
|
2522
|
+
* What a failed `verify` means for the person, read from the worker's answer.
|
|
2523
|
+
*
|
|
2524
|
+
* The HTTP status carries the case — `code` is `VALIDATION_ERROR` for both a
|
|
2525
|
+
* wrong and an expired code, so it cannot tell them apart:
|
|
2526
|
+
* - 400 with `details.attemptsLeft`: wrong code, the request is still open;
|
|
2527
|
+
* - 400 without it: no open request for this email (already replaced);
|
|
2528
|
+
* - 429: the fifth wrong try destroyed the request;
|
|
2529
|
+
* - 410: the code outlived its minutes, and was destroyed too.
|
|
2530
|
+
*
|
|
2531
|
+
* Anything else — the network, a 500 — is not about the code, and must not
|
|
2532
|
+
* lock the field.
|
|
2533
|
+
*/
|
|
2534
|
+
function describeCodeFailure(error) {
|
|
2535
|
+
if (error?.status === 429) return {
|
|
2536
|
+
kind: 'exhausted',
|
|
2537
|
+
isLocked: true
|
|
2538
|
+
};
|
|
2539
|
+
if (error?.status === 410) return {
|
|
2540
|
+
kind: 'expired',
|
|
2541
|
+
isLocked: true
|
|
2542
|
+
};
|
|
2543
|
+
if (error?.status === 400) {
|
|
2544
|
+
const attemptsLeft = error?.details?.attemptsLeft;
|
|
2545
|
+
return {
|
|
2546
|
+
kind: 'wrong',
|
|
2547
|
+
isLocked: false,
|
|
2548
|
+
attemptsLeft: Number.isInteger(attemptsLeft) ? attemptsLeft : null
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
return {
|
|
2552
|
+
kind: 'other',
|
|
2553
|
+
isLocked: false,
|
|
2554
|
+
message: error?.message || null
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
/**
|
|
2559
|
+
* The notice above the code field.
|
|
2560
|
+
*
|
|
2561
|
+
* It sits ABOVE the field, not under it, and says what to do next — not only
|
|
2562
|
+
* that something failed. The field's own error line was 12px of red under a
|
|
2563
|
+
* cleared input, next to a greyed-out button: it read as a frozen screen.
|
|
2564
|
+
*
|
|
2565
|
+
* The most common cause gets named: a new code invalidates the previous one,
|
|
2566
|
+
* and the person is often reading an older email.
|
|
2567
|
+
*/
|
|
2568
|
+
function CodeFailureNotice({
|
|
2569
|
+
failure,
|
|
2570
|
+
labels
|
|
2571
|
+
}) {
|
|
2572
|
+
if (!failure) return null;
|
|
2573
|
+
const texts = {
|
|
2574
|
+
wrong: {
|
|
2575
|
+
title: labels.wrongCodeTitle || 'Código incorreto',
|
|
2576
|
+
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(' ')
|
|
2577
|
+
},
|
|
2578
|
+
exhausted: {
|
|
2579
|
+
title: labels.attemptsExhaustedTitle || 'Tentativas esgotadas',
|
|
2580
|
+
body: labels.attemptsExhausted || 'Por segurança, este código foi cancelado. Peça um novo para continuar.'
|
|
2581
|
+
},
|
|
2582
|
+
expired: {
|
|
2583
|
+
title: labels.codeExpiredTitle || 'Código expirado',
|
|
2584
|
+
body: labels.codeExpired || 'O código vale por poucos minutos. Peça um novo para continuar.'
|
|
2585
|
+
},
|
|
2586
|
+
other: {
|
|
2587
|
+
title: labels.codeFailedTitle || 'Não foi possível entrar',
|
|
2588
|
+
body: failure.message || labels.invalidCode || 'Tente de novo em instantes.'
|
|
2589
|
+
}
|
|
2590
|
+
}[failure.kind];
|
|
2591
|
+
return /*#__PURE__*/jsx(Alert, {
|
|
2592
|
+
color: "red",
|
|
2593
|
+
variant: "light",
|
|
2594
|
+
radius: 0,
|
|
2595
|
+
icon: /*#__PURE__*/jsx(IconAlertCircle, {
|
|
2596
|
+
size: 18
|
|
2597
|
+
}),
|
|
2598
|
+
title: texts.title
|
|
2599
|
+
/*
|
|
2600
|
+
* `role="alert"` is Mantine's default and is what makes a screen
|
|
2601
|
+
* reader announce the failure without the person moving focus away
|
|
2602
|
+
* from the field they are about to retype in.
|
|
2603
|
+
*/,
|
|
2604
|
+
styles: {
|
|
2605
|
+
root: {
|
|
2606
|
+
border: '1px solid var(--mantine-color-red-2)'
|
|
2607
|
+
}
|
|
2608
|
+
},
|
|
2609
|
+
children: /*#__PURE__*/jsx(Text, {
|
|
2610
|
+
size: "sm",
|
|
2611
|
+
lh: 1.45,
|
|
2612
|
+
children: texts.body
|
|
2613
|
+
})
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2521
2617
|
// The OAuth flow's pass-through screen.
|
|
2522
2618
|
//
|
|
2523
2619
|
// The panel is not the destination here: the user is authorizing an MCP client
|
|
@@ -2630,7 +2726,9 @@ function SignIn({
|
|
|
2630
2726
|
// present it again: it is the (email, code) pair the server validates.
|
|
2631
2727
|
const [sentTo, setSentTo] = useState(null);
|
|
2632
2728
|
const [code, setCode] = useState('');
|
|
2633
|
-
const [
|
|
2729
|
+
const [codeFailure, setCodeFailure] = useState(null);
|
|
2730
|
+
const [isCodeResent, setIsCodeResent] = useState(false);
|
|
2731
|
+
const codeInputRef = useRef(null);
|
|
2634
2732
|
|
|
2635
2733
|
// Read once, on mount: the list only changes through this screen, and
|
|
2636
2734
|
// every change below writes the new list back into state.
|
|
@@ -2639,6 +2737,7 @@ function SignIn({
|
|
|
2639
2737
|
const [isManaging, setIsManaging] = useState(false);
|
|
2640
2738
|
const [pickingEmail, setPickingEmail] = useState(null);
|
|
2641
2739
|
const isShowingAccounts = recentAccounts && accounts.length > 0 && !isChoosingOther;
|
|
2740
|
+
const isCodeLocked = !!codeFailure?.isLocked;
|
|
2642
2741
|
|
|
2643
2742
|
// Hook that fetches the application's logo
|
|
2644
2743
|
const applicationLogo = useApplicationLogo();
|
|
@@ -2685,18 +2784,36 @@ function SignIn({
|
|
|
2685
2784
|
|
|
2686
2785
|
// Step 1 — ask for the code.
|
|
2687
2786
|
const handleRequest = async values => {
|
|
2688
|
-
if (sending) return;
|
|
2787
|
+
if (sending) return false;
|
|
2689
2788
|
try {
|
|
2690
2789
|
await requestCode(values.email);
|
|
2691
2790
|
setSentTo(values.email);
|
|
2692
2791
|
setCode('');
|
|
2693
|
-
|
|
2792
|
+
setCodeFailure(null);
|
|
2694
2793
|
onCodeSent?.(values.email);
|
|
2794
|
+
return true;
|
|
2695
2795
|
} catch (error) {
|
|
2696
|
-
|
|
2796
|
+
// Nothing on the card shows this one: the app's notification is
|
|
2797
|
+
// the only place the person learns the code was not sent.
|
|
2798
|
+
onError?.(error, {
|
|
2799
|
+
step: 'request',
|
|
2800
|
+
isShownOnCard: false
|
|
2801
|
+
});
|
|
2802
|
+
return false;
|
|
2697
2803
|
}
|
|
2698
2804
|
};
|
|
2699
2805
|
|
|
2806
|
+
// A new code, from the code step. The worker replaces the request, so the
|
|
2807
|
+
// attempts start over and the previous code stops working — the notice
|
|
2808
|
+
// says so, or the person keeps typing the one from the older email.
|
|
2809
|
+
const handleResend = async () => {
|
|
2810
|
+
const isSent = await handleRequest({
|
|
2811
|
+
email: sentTo
|
|
2812
|
+
});
|
|
2813
|
+
setIsCodeResent(isSent);
|
|
2814
|
+
if (isSent) codeInputRef.current?.focus();
|
|
2815
|
+
};
|
|
2816
|
+
|
|
2700
2817
|
// Step 1, from the list — the click on a saved account IS the request.
|
|
2701
2818
|
//
|
|
2702
2819
|
// An account that came in through a provider goes back to that provider,
|
|
@@ -2744,7 +2861,8 @@ function SignIn({
|
|
|
2744
2861
|
// signals that navigation was taken over — now as information, not as a
|
|
2745
2862
|
// trap.
|
|
2746
2863
|
const handleVerify = async value => {
|
|
2747
|
-
|
|
2864
|
+
setCodeFailure(null);
|
|
2865
|
+
setIsCodeResent(false);
|
|
2748
2866
|
try {
|
|
2749
2867
|
const result = await verifyCode(sentTo, value);
|
|
2750
2868
|
|
|
@@ -2759,12 +2877,19 @@ function SignIn({
|
|
|
2759
2877
|
redirectHandled: !!target
|
|
2760
2878
|
});
|
|
2761
2879
|
} catch (error) {
|
|
2762
|
-
// The
|
|
2763
|
-
//
|
|
2764
|
-
//
|
|
2765
|
-
|
|
2880
|
+
// The failure belongs to this card, not to the global notification:
|
|
2881
|
+
// the person is looking at the eight characters they just typed.
|
|
2882
|
+
// The field goes back empty and focused, ready for the next try.
|
|
2883
|
+
setCodeFailure(describeCodeFailure(error));
|
|
2766
2884
|
setCode('');
|
|
2767
|
-
|
|
2885
|
+
codeInputRef.current?.focus();
|
|
2886
|
+
// Still reported — an app may log it — but flagged: the card
|
|
2887
|
+
// already explains it, and a notification repeating "Código
|
|
2888
|
+
// inválido" in the corner would say it twice.
|
|
2889
|
+
onError?.(error, {
|
|
2890
|
+
step: 'verify',
|
|
2891
|
+
isShownOnCard: true
|
|
2892
|
+
});
|
|
2768
2893
|
}
|
|
2769
2894
|
};
|
|
2770
2895
|
|
|
@@ -2874,7 +2999,27 @@ function SignIn({
|
|
|
2874
2999
|
})
|
|
2875
3000
|
}) : /*#__PURE__*/jsxs(Stack, {
|
|
2876
3001
|
gap: "md",
|
|
2877
|
-
children: [/*#__PURE__*/jsx(
|
|
3002
|
+
children: [isCodeResent && /*#__PURE__*/jsx(Alert, {
|
|
3003
|
+
color: "gray",
|
|
3004
|
+
variant: "light",
|
|
3005
|
+
radius: 0,
|
|
3006
|
+
p: "xs",
|
|
3007
|
+
children: /*#__PURE__*/jsxs(Text, {
|
|
3008
|
+
size: "xs",
|
|
3009
|
+
lh: 1.4,
|
|
3010
|
+
children: [/*#__PURE__*/jsx(Text, {
|
|
3011
|
+
span: true,
|
|
3012
|
+
inherit: true,
|
|
3013
|
+
fw: 700,
|
|
3014
|
+
c: "gray.9",
|
|
3015
|
+
children: labels.codeResentTitle || 'Novo código enviado.'
|
|
3016
|
+
}), ' ', labels.codeResent || 'O anterior deixou de valer.']
|
|
3017
|
+
})
|
|
3018
|
+
}), /*#__PURE__*/jsx(CodeFailureNotice, {
|
|
3019
|
+
failure: codeFailure,
|
|
3020
|
+
labels: labels
|
|
3021
|
+
}), /*#__PURE__*/jsx(TextInput, {
|
|
3022
|
+
ref: codeInputRef,
|
|
2878
3023
|
label: labels.codeLabel || 'Código de acesso'
|
|
2879
3024
|
/*
|
|
2880
3025
|
* The email is the field's description, not the subtitle:
|
|
@@ -2894,26 +3039,56 @@ function SignIn({
|
|
|
2894
3039
|
value: code,
|
|
2895
3040
|
onChange: event => {
|
|
2896
3041
|
setCode(event.currentTarget.value);
|
|
2897
|
-
|
|
3042
|
+
// Typing again is the correction: the notice has
|
|
3043
|
+
// done its job. A locked field cannot be typed in,
|
|
3044
|
+
// so an exhausted or expired notice stays.
|
|
3045
|
+
if (codeFailure) setCodeFailure(null);
|
|
2898
3046
|
},
|
|
2899
3047
|
onKeyDown: event => {
|
|
2900
3048
|
if (event.key === 'Enter' && code.trim()) handleVerify(code);
|
|
2901
3049
|
},
|
|
2902
3050
|
autoFocus: true,
|
|
2903
3051
|
autoComplete: "one-time-code",
|
|
2904
|
-
readOnly: verifying
|
|
2905
|
-
|
|
2906
|
-
|
|
3052
|
+
readOnly: verifying
|
|
3053
|
+
/*
|
|
3054
|
+
* No `error` on the field: the notice above carries the
|
|
3055
|
+
* failure, and a red border around an empty field turned
|
|
3056
|
+
* the PLACEHOLDER red — "ABCD-EFGH" read as the code the
|
|
3057
|
+
* person had typed. With the request gone there is
|
|
3058
|
+
* nothing left to type into.
|
|
3059
|
+
*/,
|
|
3060
|
+
disabled: isCodeLocked
|
|
3061
|
+
}), isCodeLocked ?
|
|
3062
|
+
/*#__PURE__*/
|
|
3063
|
+
/*
|
|
3064
|
+
* The request is gone: confirming can only fail again, so
|
|
3065
|
+
* the one action that works takes the button's place.
|
|
3066
|
+
*/
|
|
3067
|
+
jsx(Button, {
|
|
3068
|
+
type: "button",
|
|
3069
|
+
fullWidth: true,
|
|
3070
|
+
"aria-disabled": sending,
|
|
3071
|
+
onClick: sending ? undefined : handleResend,
|
|
3072
|
+
leftSection: sending ? /*#__PURE__*/jsx(Loader, {
|
|
3073
|
+
size: 14,
|
|
3074
|
+
color: "gray.0"
|
|
3075
|
+
}) : /*#__PURE__*/jsx(IconRefresh, {
|
|
3076
|
+
size: 16
|
|
3077
|
+
}),
|
|
3078
|
+
children: sending ? labels.sendingCode || 'Enviando…' : labels.sendNewCode || 'Enviar novo código'
|
|
3079
|
+
}) : /*#__PURE__*/jsx(Button, {
|
|
2907
3080
|
type: "button",
|
|
2908
3081
|
fullWidth: true
|
|
2909
|
-
// Same reason as the previous step: `disabled` would
|
|
2910
|
-
// the button exactly while signing in happens.
|
|
2911
|
-
//
|
|
2912
|
-
//
|
|
3082
|
+
// Same reason as the previous step: `disabled` would
|
|
3083
|
+
// fade the button exactly while signing in happens.
|
|
3084
|
+
//
|
|
3085
|
+
// Never disabled for an empty field either. Right
|
|
3086
|
+
// after a failure the field is empty on purpose, and
|
|
3087
|
+
// a grey button there read as a frozen screen; the
|
|
3088
|
+
// click sends the cursor to the field instead.
|
|
2913
3089
|
,
|
|
2914
3090
|
"aria-disabled": verifying,
|
|
2915
|
-
|
|
2916
|
-
onClick: verifying ? undefined : () => handleVerify(code),
|
|
3091
|
+
onClick: verifying ? undefined : () => code.trim() ? handleVerify(code) : codeInputRef.current?.focus(),
|
|
2917
3092
|
leftSection: verifying ? /*#__PURE__*/jsx(Loader, {
|
|
2918
3093
|
size: 14,
|
|
2919
3094
|
color: "gray.0"
|
|
@@ -2931,18 +3106,17 @@ function SignIn({
|
|
|
2931
3106
|
onClick: () => {
|
|
2932
3107
|
setSentTo(null);
|
|
2933
3108
|
setCode('');
|
|
2934
|
-
|
|
3109
|
+
setCodeFailure(null);
|
|
3110
|
+
setIsCodeResent(false);
|
|
2935
3111
|
// The label promises another email: the form,
|
|
2936
3112
|
// not the list the person may have come from.
|
|
2937
3113
|
setIsChoosingOther(true);
|
|
2938
3114
|
},
|
|
2939
3115
|
children: labels.changeEmail || 'Usar outro e-mail'
|
|
2940
|
-
}), /*#__PURE__*/jsx(Anchor, {
|
|
3116
|
+
}), !isCodeLocked && /*#__PURE__*/jsx(Anchor, {
|
|
2941
3117
|
size: "sm",
|
|
2942
3118
|
c: "dimmed",
|
|
2943
|
-
onClick: sending ? undefined :
|
|
2944
|
-
email: sentTo
|
|
2945
|
-
}),
|
|
3119
|
+
onClick: sending ? undefined : handleResend,
|
|
2946
3120
|
children: sending ? labels.sendingCode || 'Enviando…' : labels.resendCode || 'Reenviar código'
|
|
2947
3121
|
})]
|
|
2948
3122
|
})]
|