@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/dist/index.js
CHANGED
|
@@ -2520,6 +2520,102 @@ function TermsNotice({
|
|
|
2520
2520
|
});
|
|
2521
2521
|
}
|
|
2522
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
|
+
|
|
2523
2619
|
// The OAuth flow's pass-through screen.
|
|
2524
2620
|
//
|
|
2525
2621
|
// The panel is not the destination here: the user is authorizing an MCP client
|
|
@@ -2632,7 +2728,9 @@ function SignIn({
|
|
|
2632
2728
|
// present it again: it is the (email, code) pair the server validates.
|
|
2633
2729
|
const [sentTo, setSentTo] = react.useState(null);
|
|
2634
2730
|
const [code, setCode] = react.useState('');
|
|
2635
|
-
const [
|
|
2731
|
+
const [codeFailure, setCodeFailure] = react.useState(null);
|
|
2732
|
+
const [isCodeResent, setIsCodeResent] = react.useState(false);
|
|
2733
|
+
const codeInputRef = react.useRef(null);
|
|
2636
2734
|
|
|
2637
2735
|
// Read once, on mount: the list only changes through this screen, and
|
|
2638
2736
|
// every change below writes the new list back into state.
|
|
@@ -2641,6 +2739,7 @@ function SignIn({
|
|
|
2641
2739
|
const [isManaging, setIsManaging] = react.useState(false);
|
|
2642
2740
|
const [pickingEmail, setPickingEmail] = react.useState(null);
|
|
2643
2741
|
const isShowingAccounts = recentAccounts && accounts.length > 0 && !isChoosingOther;
|
|
2742
|
+
const isCodeLocked = !!codeFailure?.isLocked;
|
|
2644
2743
|
|
|
2645
2744
|
// Hook that fetches the application's logo
|
|
2646
2745
|
const applicationLogo = useApplicationLogo();
|
|
@@ -2687,18 +2786,36 @@ function SignIn({
|
|
|
2687
2786
|
|
|
2688
2787
|
// Step 1 — ask for the code.
|
|
2689
2788
|
const handleRequest = async values => {
|
|
2690
|
-
if (sending) return;
|
|
2789
|
+
if (sending) return false;
|
|
2691
2790
|
try {
|
|
2692
2791
|
await requestCode(values.email);
|
|
2693
2792
|
setSentTo(values.email);
|
|
2694
2793
|
setCode('');
|
|
2695
|
-
|
|
2794
|
+
setCodeFailure(null);
|
|
2696
2795
|
onCodeSent?.(values.email);
|
|
2796
|
+
return true;
|
|
2697
2797
|
} catch (error) {
|
|
2698
|
-
|
|
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;
|
|
2699
2805
|
}
|
|
2700
2806
|
};
|
|
2701
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
|
+
|
|
2702
2819
|
// Step 1, from the list — the click on a saved account IS the request.
|
|
2703
2820
|
//
|
|
2704
2821
|
// An account that came in through a provider goes back to that provider,
|
|
@@ -2746,7 +2863,8 @@ function SignIn({
|
|
|
2746
2863
|
// signals that navigation was taken over — now as information, not as a
|
|
2747
2864
|
// trap.
|
|
2748
2865
|
const handleVerify = async value => {
|
|
2749
|
-
|
|
2866
|
+
setCodeFailure(null);
|
|
2867
|
+
setIsCodeResent(false);
|
|
2750
2868
|
try {
|
|
2751
2869
|
const result = await verifyCode(sentTo, value);
|
|
2752
2870
|
|
|
@@ -2761,12 +2879,19 @@ function SignIn({
|
|
|
2761
2879
|
redirectHandled: !!target
|
|
2762
2880
|
});
|
|
2763
2881
|
} catch (error) {
|
|
2764
|
-
// The
|
|
2765
|
-
//
|
|
2766
|
-
//
|
|
2767
|
-
|
|
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));
|
|
2768
2886
|
setCode('');
|
|
2769
|
-
|
|
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
|
+
});
|
|
2770
2895
|
}
|
|
2771
2896
|
};
|
|
2772
2897
|
|
|
@@ -2876,7 +3001,27 @@ function SignIn({
|
|
|
2876
3001
|
})
|
|
2877
3002
|
}) : /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
|
|
2878
3003
|
gap: "md",
|
|
2879
|
-
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,
|
|
2880
3025
|
label: labels.codeLabel || 'Código de acesso'
|
|
2881
3026
|
/*
|
|
2882
3027
|
* The email is the field's description, not the subtitle:
|
|
@@ -2896,26 +3041,56 @@ function SignIn({
|
|
|
2896
3041
|
value: code,
|
|
2897
3042
|
onChange: event => {
|
|
2898
3043
|
setCode(event.currentTarget.value);
|
|
2899
|
-
|
|
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);
|
|
2900
3048
|
},
|
|
2901
3049
|
onKeyDown: event => {
|
|
2902
3050
|
if (event.key === 'Enter' && code.trim()) handleVerify(code);
|
|
2903
3051
|
},
|
|
2904
3052
|
autoFocus: true,
|
|
2905
3053
|
autoComplete: "one-time-code",
|
|
2906
|
-
readOnly: verifying
|
|
2907
|
-
|
|
2908
|
-
|
|
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, {
|
|
2909
3082
|
type: "button",
|
|
2910
3083
|
fullWidth: true
|
|
2911
|
-
// Same reason as the previous step: `disabled` would
|
|
2912
|
-
// the button exactly while signing in happens.
|
|
2913
|
-
//
|
|
2914
|
-
//
|
|
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.
|
|
2915
3091
|
,
|
|
2916
3092
|
"aria-disabled": verifying,
|
|
2917
|
-
|
|
2918
|
-
onClick: verifying ? undefined : () => handleVerify(code),
|
|
3093
|
+
onClick: verifying ? undefined : () => code.trim() ? handleVerify(code) : codeInputRef.current?.focus(),
|
|
2919
3094
|
leftSection: verifying ? /*#__PURE__*/jsxRuntime.jsx(core.Loader, {
|
|
2920
3095
|
size: 14,
|
|
2921
3096
|
color: "gray.0"
|
|
@@ -2933,18 +3108,17 @@ function SignIn({
|
|
|
2933
3108
|
onClick: () => {
|
|
2934
3109
|
setSentTo(null);
|
|
2935
3110
|
setCode('');
|
|
2936
|
-
|
|
3111
|
+
setCodeFailure(null);
|
|
3112
|
+
setIsCodeResent(false);
|
|
2937
3113
|
// The label promises another email: the form,
|
|
2938
3114
|
// not the list the person may have come from.
|
|
2939
3115
|
setIsChoosingOther(true);
|
|
2940
3116
|
},
|
|
2941
3117
|
children: labels.changeEmail || 'Usar outro e-mail'
|
|
2942
|
-
}), /*#__PURE__*/jsxRuntime.jsx(core.Anchor, {
|
|
3118
|
+
}), !isCodeLocked && /*#__PURE__*/jsxRuntime.jsx(core.Anchor, {
|
|
2943
3119
|
size: "sm",
|
|
2944
3120
|
c: "dimmed",
|
|
2945
|
-
onClick: sending ? undefined :
|
|
2946
|
-
email: sentTo
|
|
2947
|
-
}),
|
|
3121
|
+
onClick: sending ? undefined : handleResend,
|
|
2948
3122
|
children: sending ? labels.sendingCode || 'Enviando…' : labels.resendCode || 'Reenviar código'
|
|
2949
3123
|
})]
|
|
2950
3124
|
})]
|