@vvedantb/native-ui 0.17.2 → 0.18.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/dist/AlertDialog.jsx +123 -0
- package/dist/AmbientBackground.jsx +73 -0
- package/dist/ArtifactList.jsx +59 -0
- package/dist/Avatar.jsx +285 -0
- package/dist/Badge.jsx +154 -0
- package/dist/BottomSheetContainer.jsx +208 -0
- package/dist/BulkActionBar.jsx +717 -0
- package/dist/Button.jsx +344 -0
- package/dist/CaptureButton.jsx +104 -0
- package/dist/Card.jsx +371 -0
- package/dist/Checkbox.jsx +177 -0
- package/dist/Chip.jsx +79 -0
- package/dist/ColorSwatches.jsx +159 -0
- package/dist/ConfirmDialog.jsx +157 -0
- package/dist/DateTimeField.jsx +204 -0
- package/dist/Dialog.jsx +614 -0
- package/dist/DividedList.jsx +120 -0
- package/dist/DropdownMenu.jsx +849 -0
- package/dist/EmptyState.jsx +434 -0
- package/dist/ExpoWebRoot.jsx +134 -0
- package/dist/FloatingFAB.jsx +95 -0
- package/dist/FloatingSurface.jsx +226 -0
- package/dist/FloatingTabBar.jsx +403 -0
- package/dist/HeaderIconButton.jsx +53 -0
- package/dist/IconButton.jsx +157 -0
- package/dist/Input.jsx +96 -0
- package/dist/Label.jsx +96 -0
- package/dist/LabeledSwitchRow.jsx +170 -0
- package/dist/LiquidGlass.jsx +2 -0
- package/dist/ListRow.jsx +281 -0
- package/dist/LoadingState.jsx +205 -0
- package/dist/PageIndicator.jsx +128 -0
- package/dist/PageTitle.jsx +205 -0
- package/dist/Pager.jsx +105 -0
- package/dist/PasswordInput.jsx +120 -0
- package/dist/PillTabs.jsx +300 -0
- package/dist/PinnedBotTile.jsx +212 -0
- package/dist/Progress.jsx +147 -0
- package/dist/SafeAreaContainer.jsx +38 -0
- package/dist/ScreenContainer.jsx +117 -0
- package/dist/SearchInput.jsx +150 -0
- package/dist/SectionLabel.jsx +128 -0
- package/dist/Select.jsx +223 -0
- package/dist/Separator.jsx +95 -0
- package/dist/Sheet.jsx +343 -0
- package/dist/SheetRow.jsx +198 -0
- package/dist/Skeleton.jsx +311 -0
- package/dist/Spinner.jsx +195 -0
- package/dist/StackPageHeader.jsx +160 -0
- package/dist/StatGrid.jsx +74 -0
- package/dist/StatusDot.jsx +129 -0
- package/dist/SurfaceRow.jsx +143 -0
- package/dist/SwipeableRow.jsx +50 -0
- package/dist/Switch.jsx +130 -0
- package/dist/Tabs.jsx +385 -0
- package/dist/Textarea.jsx +103 -0
- package/dist/ThemeToggle.jsx +78 -0
- package/dist/Toast.jsx +396 -0
- package/dist/ai/activity-step-detail.jsx +179 -0
- package/dist/ai/activity-step-label.js +816 -0
- package/dist/ai/activity-step-types.js +14 -0
- package/dist/ai/activity-step.jsx +98 -0
- package/dist/ai/activity-tasks-utils.js +172 -0
- package/dist/ai/activity-timeline.js +47 -0
- package/dist/ai/attachments.jsx +258 -0
- package/dist/ai/card-button.jsx +188 -0
- package/dist/ai/chain-of-thought.jsx +459 -0
- package/dist/ai/chat-jump-rail.jsx +57 -0
- package/dist/ai/chat-text.js +44 -0
- package/dist/ai/chat-thread.jsx +135 -0
- package/dist/ai/choice-question.jsx +925 -0
- package/dist/ai/code-block.jsx +574 -0
- package/dist/ai/collapsible.jsx +275 -0
- package/dist/ai/conversation.jsx +723 -0
- package/dist/ai/file-chip.jsx +198 -0
- package/dist/ai/icon-button.jsx +179 -0
- package/dist/ai/index.js +37 -0
- package/dist/ai/message-action-sheet.jsx +408 -0
- package/dist/ai/message-quote.jsx +241 -0
- package/dist/ai/message.jsx +739 -0
- package/dist/ai/prompt-input-menu.jsx +365 -0
- package/dist/ai/prompt-input.jsx +626 -0
- package/dist/ai/reasoning.jsx +446 -0
- package/dist/ai/reply-quote-chip.jsx +278 -0
- package/dist/ai/response-text.jsx +130 -0
- package/dist/ai/shimmer.jsx +149 -0
- package/dist/ai/sources.jsx +260 -0
- package/dist/ai/subagent-cards.jsx +645 -0
- package/dist/ai/suggestion.jsx +186 -0
- package/dist/ai/tool-approval-card.jsx +359 -0
- package/dist/ai/trace.jsx +728 -0
- package/dist/ai/unified-diff.jsx +280 -0
- package/dist/ai/use-streaming-disclosure.js +70 -0
- package/dist/auth/AuthProviders.jsx +27 -0
- package/dist/auth/AuthScreen.jsx +292 -0
- package/dist/auth/GoogleSignInButton.jsx +188 -0
- package/dist/auth/SecondFactorScreen.jsx +133 -0
- package/dist/auth/SignInScreen.jsx +482 -0
- package/dist/auth/authErrors.js +131 -0
- package/dist/auth/index.js +11 -0
- package/dist/auth/totp.js +378 -0
- package/dist/blur.js +36 -0
- package/dist/brand/AppIcons.jsx +118 -0
- package/dist/brand/AppMark.jsx +128 -0
- package/dist/brand/AppWordmark.jsx +138 -0
- package/dist/brand/GoogleMark.jsx +71 -0
- package/dist/brand/ProviderIcon.jsx +192 -0
- package/dist/brand/familyApps.js +33 -0
- package/dist/brand/index.js +11 -0
- package/dist/brand/svg.js +252 -0
- package/dist/brand/vProAppBenefits.js +19 -0
- package/dist/cn.js +5 -0
- package/dist/colors.js +15 -0
- package/dist/configureTheme.js +72 -0
- package/dist/controlStyles.js +520 -0
- package/dist/definiteSize.js +12 -0
- package/dist/dialogLayout.js +18 -0
- package/dist/entries/ai.js +4 -0
- package/dist/entries/ambient.js +3 -0
- package/dist/entries/auth.js +5 -0
- package/dist/entries/bottom-sheet.js +2 -0
- package/dist/entries/brand.js +2 -0
- package/dist/entries/button.js +2 -0
- package/dist/entries/date-time.js +3 -0
- package/dist/entries/expo-router.js +4 -0
- package/dist/entries/expo-web-root.js +3 -0
- package/dist/entries/floating.js +7 -0
- package/dist/entries/gesture-handler.js +4 -0
- package/dist/entries/keyboard-controller.js +4 -0
- package/dist/entries/layout.js +16 -0
- package/dist/entries/markdown.js +4 -0
- package/dist/entries/marketing.js +3 -0
- package/dist/entries/page-title.js +7 -0
- package/dist/entries/password-input.js +3 -0
- package/dist/entries/primitives.js +57 -0
- package/dist/entries/search-input.js +3 -0
- package/dist/entries/tab-bar.js +4 -0
- package/dist/entries/theme.js +7 -0
- package/dist/entries/toast.js +5 -0
- package/dist/entries/video.js +3 -0
- package/dist/entries/webview.js +4 -0
- package/dist/examples/AiChatGallery.jsx +509 -0
- package/dist/examples/ParityGallery.jsx +1307 -0
- package/dist/floating.js +62 -0
- package/dist/glassBlur.jsx +145 -0
- package/dist/gradient.js +17 -0
- package/dist/haptics.js +44 -0
- package/dist/highlight.js +382 -0
- package/dist/index.js +85 -0
- package/dist/initials.js +4 -0
- package/dist/loadingGrid.js +81 -0
- package/dist/markdown/Markdown.jsx +515 -0
- package/dist/markdown/MarkdownTable.jsx +443 -0
- package/dist/markdown/tableText.js +68 -0
- package/dist/marketing/PricingModal.jsx +608 -0
- package/dist/marketing/UpgradeToProCard.jsx +164 -0
- package/dist/marketing/pricing.js +34 -0
- package/dist/media/ImageGallery.jsx +580 -0
- package/dist/media/MediaCard.jsx +162 -0
- package/dist/media/imageGrid.js +54 -0
- package/dist/motion.js +33 -0
- package/dist/pagerSwipe.js +104 -0
- package/dist/palette.js +104 -0
- package/dist/primitives/button.jsx +127 -0
- package/dist/primitives/card.jsx +249 -0
- package/dist/primitives/icon.jsx +37 -0
- package/dist/primitives/input.jsx +56 -0
- package/dist/primitives/keyboard-controller.jsx +207 -0
- package/dist/primitives/keyboard-parts.jsx +75 -0
- package/dist/primitives/native-only-animated-view.jsx +68 -0
- package/dist/primitives/overlay.jsx +391 -0
- package/dist/primitives/pager-gesture.jsx +131 -0
- package/dist/primitives/select-host.jsx +103 -0
- package/dist/primitives/select.jsx +555 -0
- package/dist/primitives/sheet-drag.jsx +420 -0
- package/dist/primitives/sheet-gesture.jsx +219 -0
- package/dist/primitives/swipe-row-gesture.jsx +341 -0
- package/dist/primitives/switch.jsx +105 -0
- package/dist/primitives/text.jsx +145 -0
- package/dist/primitives/toast-drag-gesture.jsx +94 -0
- package/dist/primitives/use-controllable-state.js +40 -0
- package/dist/primitives/web-click-guard.js +69 -0
- package/dist/push.js +166 -0
- package/dist/relativeTime.js +70 -0
- package/dist/router/RouterStackPageHeader.jsx +53 -0
- package/dist/selectA11y.js +32 -0
- package/dist/settings/MoreFromProjectV.jsx +168 -0
- package/dist/settings/SettingsEmptyState.jsx +102 -0
- package/dist/settings/SettingsField.jsx +49 -0
- package/dist/settings/SettingsSection.jsx +101 -0
- package/dist/settings/SettingsStack.jsx +17 -0
- package/dist/settings/SettingsToggleRow.jsx +163 -0
- package/dist/settings/projectVMobileApps.js +126 -0
- package/dist/settings/relaxedSizeStyle.js +11 -0
- package/dist/sheetDrag.js +76 -0
- package/dist/svgA11y.js +46 -0
- package/dist/swipeRow.js +161 -0
- package/dist/tabBarRoutes.js +41 -0
- package/dist/tabIndicator.js +139 -0
- package/dist/tailwind-preset.cjs +255 -0
- package/dist/tailwind-preset.d.cts +55 -0
- package/dist/theme.jsx +62 -0
- package/dist/toastChrome.js +110 -0
- package/dist/toastDrag.js +51 -0
- package/dist/toastStore.js +54 -0
- package/dist/tokens.js +70 -0
- package/dist/typography.js +40 -0
- package/dist/useReduceMotion.js +32 -0
- package/dist/useReduceTransparency.js +27 -0
- package/dist/useScheme.js +15 -0
- package/dist/video/InlineVideo.jsx +115 -0
- package/dist/webview/HtmlView.jsx +169 -0
- package/dist/webview/HtmlView.web.jsx +63 -0
- package/dist/webview/htmlView.js +43 -0
- package/package.json +70 -33
- package/src/DropdownMenu.tsx +28 -16
- package/src/FloatingTabBar.tsx +78 -41
- package/src/ai/conversation.tsx +12 -8
- package/src/glassBlur.tsx +47 -22
- package/src/primitives/swipe-row-gesture.tsx +11 -2
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { c as _c } from "react/compiler-runtime";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { Text, View } from "react-native";
|
|
4
|
+
import { useSignIn, useSignUp } from "@clerk/clerk-expo";
|
|
5
|
+
import { useRouter } from "expo-router";
|
|
6
|
+
import { Button } from "../Button";
|
|
7
|
+
import { textStyle } from "../controlStyles";
|
|
8
|
+
import { Input } from "../Input";
|
|
9
|
+
import { PasswordInput } from "../PasswordInput";
|
|
10
|
+
import { toast } from "../toastStore";
|
|
11
|
+
import { useScheme } from "../useScheme";
|
|
12
|
+
import { AuthScreen, FieldError, OrDivider } from "./AuthScreen";
|
|
13
|
+
import { authErrorMessage, DEFAULT_STAGE_TIMEOUT_MS, isAuthTimeoutError, logAuthFailure, withAuthTimeout } from "./authErrors";
|
|
14
|
+
import { SecondFactorScreen } from "./SecondFactorScreen";
|
|
15
|
+
import { pickFirstFactorStrategy, pickSecondFactorStrategy, prepareFirstFactorIfNeeded, submitFirstFactor, supportedFirstFactorStrategies, supportedSecondFactorStrategies, useSecondFactor } from "./totp";
|
|
16
|
+
import { GoogleSignInButton } from "./GoogleSignInButton";
|
|
17
|
+
/** The messages the screen can show. Generic: none names a provider. */
|
|
18
|
+
export const AUTH_MESSAGES = {
|
|
19
|
+
missingFields: "Enter your email and password",
|
|
20
|
+
signInFailed: "Sign in failed",
|
|
21
|
+
invalidCredentials: "Invalid email or password",
|
|
22
|
+
signUpFailed: "Could not create account",
|
|
23
|
+
verifyFailed: "Invalid code",
|
|
24
|
+
missingCode: "Enter the code from the email",
|
|
25
|
+
needsAnotherStep: "This account needs another step to sign in. Finish it on the web.",
|
|
26
|
+
secondFactorTitle: "Two-step verification",
|
|
27
|
+
secondFactorDescription: "Enter the code from your authenticator app.",
|
|
28
|
+
emailCodeTitle: "Check your email",
|
|
29
|
+
emailCodeDescription: "Enter the code we sent to your email.",
|
|
30
|
+
phoneCodeTitle: "Check your phone",
|
|
31
|
+
phoneCodeDescription: "Enter the code we sent by text message.",
|
|
32
|
+
backupCodeTitle: "Enter a backup code",
|
|
33
|
+
backupCodeDescription: "Use one of the backup codes you saved when you set up two-step verification.",
|
|
34
|
+
clientTrustTitle: "Verify it's you",
|
|
35
|
+
clientTrustEmailDescription: "This device is new. Enter the code we sent to your email.",
|
|
36
|
+
clientTrustPhoneDescription: "This device is new. Enter the code we sent by text message.",
|
|
37
|
+
useBackupCode: "Use a backup code",
|
|
38
|
+
useAuthenticator: "Use authenticator app",
|
|
39
|
+
useEmailCode: "Email me a code",
|
|
40
|
+
usePhoneCode: "Text me a code",
|
|
41
|
+
newPasswordTitle: "Set a new password",
|
|
42
|
+
newPasswordDescription: "Your password must change before you can sign in.",
|
|
43
|
+
newPasswordLabel: "New password",
|
|
44
|
+
confirmPasswordLabel: "Confirm new password",
|
|
45
|
+
passwordMismatch: "The passwords do not match",
|
|
46
|
+
passwordTooShort: "Use a longer password",
|
|
47
|
+
resetPasswordFailed: "Could not set the new password",
|
|
48
|
+
setPassword: "Set password"
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A sign-in that is not complete, as `signIn.create` returns it. Passed to
|
|
53
|
+
* `onIncomplete` so the app can run the step itself.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const METHOD_LABEL = {
|
|
57
|
+
totp: AUTH_MESSAGES.useAuthenticator,
|
|
58
|
+
email_code: AUTH_MESSAGES.useEmailCode,
|
|
59
|
+
phone_code: AUTH_MESSAGES.usePhoneCode,
|
|
60
|
+
backup_code: AUTH_MESSAGES.useBackupCode
|
|
61
|
+
};
|
|
62
|
+
function secondFactorCopy(strategy, clientTrust) {
|
|
63
|
+
if (strategy === "email_code") {
|
|
64
|
+
return clientTrust ? {
|
|
65
|
+
title: AUTH_MESSAGES.clientTrustTitle,
|
|
66
|
+
description: AUTH_MESSAGES.clientTrustEmailDescription
|
|
67
|
+
} : {
|
|
68
|
+
title: AUTH_MESSAGES.emailCodeTitle,
|
|
69
|
+
description: AUTH_MESSAGES.emailCodeDescription
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (strategy === "phone_code") {
|
|
73
|
+
return clientTrust ? {
|
|
74
|
+
title: AUTH_MESSAGES.clientTrustTitle,
|
|
75
|
+
description: AUTH_MESSAGES.clientTrustPhoneDescription
|
|
76
|
+
} : {
|
|
77
|
+
title: AUTH_MESSAGES.phoneCodeTitle,
|
|
78
|
+
description: AUTH_MESSAGES.phoneCodeDescription
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (strategy === "backup_code") {
|
|
82
|
+
return {
|
|
83
|
+
title: AUTH_MESSAGES.backupCodeTitle,
|
|
84
|
+
description: AUTH_MESSAGES.backupCodeDescription
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
title: AUTH_MESSAGES.secondFactorTitle,
|
|
89
|
+
description: AUTH_MESSAGES.secondFactorDescription
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Email and password, and Google, sign-in or sign-up. Sign-up sends an email
|
|
94
|
+
* code and checks it before the session starts. On success it calls
|
|
95
|
+
* `onSuccess`, or `router.replace(afterAuthHref, replaceOptions)`.
|
|
96
|
+
*
|
|
97
|
+
* Forgot-password is not here: an app that needs it keeps its own screen.
|
|
98
|
+
*/
|
|
99
|
+
export function SignInScreen({
|
|
100
|
+
mode = "sign-in",
|
|
101
|
+
title,
|
|
102
|
+
subtitle,
|
|
103
|
+
scheme,
|
|
104
|
+
redirectPath,
|
|
105
|
+
afterAuthHref = "/",
|
|
106
|
+
replaceOptions,
|
|
107
|
+
onSuccess,
|
|
108
|
+
showGoogle = true,
|
|
109
|
+
googleLabel,
|
|
110
|
+
signUpHref,
|
|
111
|
+
signInHref,
|
|
112
|
+
minPasswordLength,
|
|
113
|
+
errorDisplay = "toast",
|
|
114
|
+
testIDPrefix,
|
|
115
|
+
header,
|
|
116
|
+
footer,
|
|
117
|
+
onIncomplete,
|
|
118
|
+
stageTimeoutMs = DEFAULT_STAGE_TIMEOUT_MS
|
|
119
|
+
}) {
|
|
120
|
+
const signInState = useSignIn();
|
|
121
|
+
const signUpState = useSignUp();
|
|
122
|
+
const router = useRouter();
|
|
123
|
+
const colorScheme = useScheme();
|
|
124
|
+
const [email, setEmail] = useState("");
|
|
125
|
+
const [password, setPassword] = useState("");
|
|
126
|
+
const [code, setCode] = useState("");
|
|
127
|
+
const [pendingVerification, setPendingVerification] = useState(false);
|
|
128
|
+
const [pendingSecondFactor, setPendingSecondFactor] = useState(false);
|
|
129
|
+
const [pendingFirstFactor, setPendingFirstFactor] = useState(false);
|
|
130
|
+
const [firstFactors, setFirstFactors] = useState([]);
|
|
131
|
+
const [firstStrategy, setFirstStrategy] = useState("email_code");
|
|
132
|
+
const [firstLoading, setFirstLoading] = useState(false);
|
|
133
|
+
const [firstError, setFirstError] = useState("");
|
|
134
|
+
const [factors, setFactors] = useState([]);
|
|
135
|
+
const [clientTrust, setClientTrust] = useState(false);
|
|
136
|
+
const [pendingNewPassword, setPendingNewPassword] = useState(false);
|
|
137
|
+
const [newPassword, setNewPassword] = useState("");
|
|
138
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
139
|
+
const secondFactor = useSecondFactor({
|
|
140
|
+
stageTimeoutMs
|
|
141
|
+
});
|
|
142
|
+
const [error, setError] = useState("");
|
|
143
|
+
const [submitting, setSubmitting] = useState(false);
|
|
144
|
+
// The field each "next" key moves to.
|
|
145
|
+
const passwordField = useRef(null);
|
|
146
|
+
const confirmField = useRef(null);
|
|
147
|
+
const signUp = mode === "sign-up";
|
|
148
|
+
const id = name => `${testIDPrefix ?? mode}-${name}`;
|
|
149
|
+
const fail = (heading, message) => {
|
|
150
|
+
if (errorDisplay === "inline") setError(message);else toast.error(heading, message === heading ? undefined : {
|
|
151
|
+
description: message
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
const done = () => {
|
|
155
|
+
if (onSuccess) onSuccess();else router.replace(afterAuthHref, replaceOptions);
|
|
156
|
+
};
|
|
157
|
+
const warnIncomplete = attempt => {
|
|
158
|
+
console.warn(`[native-ui] Sign-in not complete: status=${attempt.status}, ` + `secondFactors=${JSON.stringify((attempt.supportedSecondFactors ?? []).map(f => f.strategy))}, ` + `firstFactors=${JSON.stringify((attempt.supportedFirstFactors ?? []).map(f_0 => f_0.strategy))}. ` + "Pass onIncomplete to handle it in the app.");
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// A status the built-in UI cannot finish: the app's `onIncomplete`, or the web fallback.
|
|
162
|
+
const unhandled = attempt_0 => {
|
|
163
|
+
if (onIncomplete) return onIncomplete(attempt_0);
|
|
164
|
+
warnIncomplete(attempt_0);
|
|
165
|
+
fail(AUTH_MESSAGES.signInFailed, AUTH_MESSAGES.needsAnotherStep);
|
|
166
|
+
};
|
|
167
|
+
const chooseStrategy = async (strategy, list) => {
|
|
168
|
+
secondFactor.clearError();
|
|
169
|
+
secondFactor.setStrategy(strategy);
|
|
170
|
+
setPendingSecondFactor(true);
|
|
171
|
+
await secondFactor.prepare(strategy, list.find(factor => factor.strategy === strategy));
|
|
172
|
+
};
|
|
173
|
+
const startSecondFactor = async (attempt_1, trust) => {
|
|
174
|
+
const list_0 = attempt_1.supportedSecondFactors ?? [];
|
|
175
|
+
const strategy_0 = pickSecondFactorStrategy(list_0);
|
|
176
|
+
if (!strategy_0) return unhandled(attempt_1);
|
|
177
|
+
setFactors(list_0);
|
|
178
|
+
setClientTrust(trust);
|
|
179
|
+
await chooseStrategy(strategy_0, list_0);
|
|
180
|
+
};
|
|
181
|
+
const chooseFirstStrategy = async (strategy_1, list_1) => {
|
|
182
|
+
const {
|
|
183
|
+
signIn
|
|
184
|
+
} = signInState;
|
|
185
|
+
if (!signIn) return;
|
|
186
|
+
setFirstError("");
|
|
187
|
+
setFirstStrategy(strategy_1);
|
|
188
|
+
setPendingFirstFactor(true);
|
|
189
|
+
setFirstLoading(true);
|
|
190
|
+
const sent = await prepareFirstFactorIfNeeded({
|
|
191
|
+
signIn,
|
|
192
|
+
strategy: strategy_1,
|
|
193
|
+
factor: list_1.find(factor_0 => factor_0.strategy === strategy_1),
|
|
194
|
+
timeoutMs: stageTimeoutMs
|
|
195
|
+
});
|
|
196
|
+
if (!sent.ok) setFirstError(sent.error);
|
|
197
|
+
setFirstLoading(false);
|
|
198
|
+
};
|
|
199
|
+
const startFirstFactor = async attempt_2 => {
|
|
200
|
+
const list_2 = attempt_2.supportedFirstFactors ?? [];
|
|
201
|
+
const strategy_2 = pickFirstFactorStrategy(list_2);
|
|
202
|
+
if (!strategy_2) return unhandled(attempt_2);
|
|
203
|
+
setFirstFactors(list_2);
|
|
204
|
+
await chooseFirstStrategy(strategy_2, list_2);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Routes a sign-in attempt. `needs_first_factor` is built in only after `signIn.create`.
|
|
208
|
+
const routeAttempt = async (attempt_3, afterCreate) => {
|
|
209
|
+
const {
|
|
210
|
+
setActive
|
|
211
|
+
} = signInState;
|
|
212
|
+
// Clerk's `SignInStatus` omits `needs_client_trust`: widen before comparing.
|
|
213
|
+
const status = attempt_3.status;
|
|
214
|
+
if (status === "complete" && attempt_3.createdSessionId && setActive) {
|
|
215
|
+
await withAuthTimeout(setActive({
|
|
216
|
+
session: attempt_3.createdSessionId
|
|
217
|
+
}), "set-active", stageTimeoutMs);
|
|
218
|
+
done();
|
|
219
|
+
} else if (status === "needs_new_password") {
|
|
220
|
+
setPendingFirstFactor(false);
|
|
221
|
+
setPendingNewPassword(true);
|
|
222
|
+
} else if (status === "needs_second_factor" || status === "needs_client_trust") {
|
|
223
|
+
setPendingFirstFactor(false);
|
|
224
|
+
await startSecondFactor(attempt_3, status === "needs_client_trust");
|
|
225
|
+
} else if (status === "needs_first_factor" && afterCreate) {
|
|
226
|
+
await startFirstFactor(attempt_3);
|
|
227
|
+
} else {
|
|
228
|
+
unhandled(attempt_3);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const onFirstFactor = async firstCode => {
|
|
232
|
+
const {
|
|
233
|
+
isLoaded,
|
|
234
|
+
signIn: signIn_0
|
|
235
|
+
} = signInState;
|
|
236
|
+
if (!isLoaded || !signIn_0 || firstLoading) return;
|
|
237
|
+
setFirstLoading(true);
|
|
238
|
+
setFirstError("");
|
|
239
|
+
const result = await submitFirstFactor({
|
|
240
|
+
signIn: signIn_0,
|
|
241
|
+
code: firstCode,
|
|
242
|
+
strategy: firstStrategy,
|
|
243
|
+
timeoutMs: stageTimeoutMs
|
|
244
|
+
});
|
|
245
|
+
if (!result.ok) {
|
|
246
|
+
setFirstError(result.error);
|
|
247
|
+
setFirstLoading(false);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
await routeAttempt(result.attempt, false);
|
|
252
|
+
} catch (caught) {
|
|
253
|
+
if (isAuthTimeoutError(caught)) logAuthFailure("set-active", caught);
|
|
254
|
+
setFirstError(authErrorMessage(caught, AUTH_MESSAGES.verifyFailed));
|
|
255
|
+
} finally {
|
|
256
|
+
setFirstLoading(false);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const onNewPassword = async () => {
|
|
260
|
+
const {
|
|
261
|
+
isLoaded: isLoaded_0,
|
|
262
|
+
signIn: signIn_1,
|
|
263
|
+
setActive: setActive_0
|
|
264
|
+
} = signInState;
|
|
265
|
+
if (!isLoaded_0 || !signIn_1 || !setActive_0 || submitting) return;
|
|
266
|
+
if (!newPassword) return fail(AUTH_MESSAGES.resetPasswordFailed, AUTH_MESSAGES.missingFields);
|
|
267
|
+
if (minPasswordLength && newPassword.length < minPasswordLength) {
|
|
268
|
+
return fail(AUTH_MESSAGES.resetPasswordFailed, `Use at least ${minPasswordLength} characters for the password`);
|
|
269
|
+
}
|
|
270
|
+
if (newPassword !== confirmPassword) {
|
|
271
|
+
return fail(AUTH_MESSAGES.resetPasswordFailed, AUTH_MESSAGES.passwordMismatch);
|
|
272
|
+
}
|
|
273
|
+
setSubmitting(true);
|
|
274
|
+
setError("");
|
|
275
|
+
try {
|
|
276
|
+
const result_0 = await withAuthTimeout(signIn_1.resetPassword({
|
|
277
|
+
password: newPassword
|
|
278
|
+
}), "reset-password", stageTimeoutMs);
|
|
279
|
+
if (result_0.status === "complete" && result_0.createdSessionId) {
|
|
280
|
+
await withAuthTimeout(setActive_0({
|
|
281
|
+
session: result_0.createdSessionId
|
|
282
|
+
}), "set-active", stageTimeoutMs);
|
|
283
|
+
done();
|
|
284
|
+
} else if (onIncomplete) {
|
|
285
|
+
onIncomplete(result_0);
|
|
286
|
+
} else {
|
|
287
|
+
warnIncomplete(result_0);
|
|
288
|
+
fail(AUTH_MESSAGES.resetPasswordFailed, AUTH_MESSAGES.needsAnotherStep);
|
|
289
|
+
}
|
|
290
|
+
} catch (caught_0) {
|
|
291
|
+
if (isAuthTimeoutError(caught_0)) logAuthFailure("reset-password", caught_0);
|
|
292
|
+
fail(AUTH_MESSAGES.resetPasswordFailed, authErrorMessage(caught_0, AUTH_MESSAGES.resetPasswordFailed));
|
|
293
|
+
} finally {
|
|
294
|
+
setSubmitting(false);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const onSignIn = async () => {
|
|
298
|
+
const {
|
|
299
|
+
isLoaded: isLoaded_1,
|
|
300
|
+
signIn: signIn_2,
|
|
301
|
+
setActive: setActive_1
|
|
302
|
+
} = signInState;
|
|
303
|
+
if (!isLoaded_1 || !signIn_2 || !setActive_1 || submitting) return;
|
|
304
|
+
if (!email || !password) return fail(AUTH_MESSAGES.signInFailed, AUTH_MESSAGES.missingFields);
|
|
305
|
+
setSubmitting(true);
|
|
306
|
+
setError("");
|
|
307
|
+
try {
|
|
308
|
+
const attempt_4 = await withAuthTimeout(signIn_2.create({
|
|
309
|
+
identifier: email,
|
|
310
|
+
password
|
|
311
|
+
}), "create", stageTimeoutMs);
|
|
312
|
+
await routeAttempt(attempt_4, true);
|
|
313
|
+
} catch (caught_1) {
|
|
314
|
+
logAuthFailure("create", caught_1);
|
|
315
|
+
fail(AUTH_MESSAGES.signInFailed, authErrorMessage(caught_1, AUTH_MESSAGES.invalidCredentials, {
|
|
316
|
+
credentials: true
|
|
317
|
+
}));
|
|
318
|
+
} finally {
|
|
319
|
+
setSubmitting(false);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
const onSecondFactor = async secondFactorCode => {
|
|
323
|
+
const result_1 = await secondFactor.submit(secondFactorCode);
|
|
324
|
+
if (result_1.ok) done();
|
|
325
|
+
};
|
|
326
|
+
const onSecondFactorBack = () => {
|
|
327
|
+
// The pending attempt stays; the next `signIn.create` replaces it.
|
|
328
|
+
setPendingSecondFactor(false);
|
|
329
|
+
setPendingFirstFactor(false);
|
|
330
|
+
setFirstError("");
|
|
331
|
+
setPendingNewPassword(false);
|
|
332
|
+
setNewPassword("");
|
|
333
|
+
setConfirmPassword("");
|
|
334
|
+
setPassword("");
|
|
335
|
+
setError("");
|
|
336
|
+
secondFactor.clearError();
|
|
337
|
+
};
|
|
338
|
+
const onSignUp = async () => {
|
|
339
|
+
const {
|
|
340
|
+
isLoaded: isLoaded_2,
|
|
341
|
+
signUp: attempt_5
|
|
342
|
+
} = signUpState;
|
|
343
|
+
if (!isLoaded_2 || !attempt_5 || submitting) return;
|
|
344
|
+
if (!email || !password) return fail(AUTH_MESSAGES.signUpFailed, AUTH_MESSAGES.missingFields);
|
|
345
|
+
if (minPasswordLength && password.length < minPasswordLength) {
|
|
346
|
+
return fail(AUTH_MESSAGES.signUpFailed, `Use at least ${minPasswordLength} characters for the password`);
|
|
347
|
+
}
|
|
348
|
+
setSubmitting(true);
|
|
349
|
+
setError("");
|
|
350
|
+
try {
|
|
351
|
+
await attempt_5.create({
|
|
352
|
+
emailAddress: email,
|
|
353
|
+
password
|
|
354
|
+
});
|
|
355
|
+
await attempt_5.prepareEmailAddressVerification({
|
|
356
|
+
strategy: "email_code"
|
|
357
|
+
});
|
|
358
|
+
setPendingVerification(true);
|
|
359
|
+
} catch (caught_2) {
|
|
360
|
+
fail(AUTH_MESSAGES.signUpFailed, authErrorMessage(caught_2, AUTH_MESSAGES.signUpFailed));
|
|
361
|
+
} finally {
|
|
362
|
+
setSubmitting(false);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const onVerify = async () => {
|
|
366
|
+
const {
|
|
367
|
+
isLoaded: isLoaded_3,
|
|
368
|
+
signUp: attempt_6,
|
|
369
|
+
setActive: setActive_2
|
|
370
|
+
} = signUpState;
|
|
371
|
+
if (!isLoaded_3 || !attempt_6 || !setActive_2 || submitting) return;
|
|
372
|
+
if (!code) return fail(AUTH_MESSAGES.verifyFailed, AUTH_MESSAGES.missingCode);
|
|
373
|
+
setSubmitting(true);
|
|
374
|
+
setError("");
|
|
375
|
+
try {
|
|
376
|
+
const result_2 = await attempt_6.attemptEmailAddressVerification({
|
|
377
|
+
code
|
|
378
|
+
});
|
|
379
|
+
if (result_2.status === "complete" && result_2.createdSessionId) {
|
|
380
|
+
await setActive_2({
|
|
381
|
+
session: result_2.createdSessionId
|
|
382
|
+
});
|
|
383
|
+
done();
|
|
384
|
+
} else {
|
|
385
|
+
fail(AUTH_MESSAGES.verifyFailed, AUTH_MESSAGES.needsAnotherStep);
|
|
386
|
+
}
|
|
387
|
+
} catch (caught_3) {
|
|
388
|
+
fail(AUTH_MESSAGES.verifyFailed, authErrorMessage(caught_3, AUTH_MESSAGES.verifyFailed));
|
|
389
|
+
} finally {
|
|
390
|
+
setSubmitting(false);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
const linkHref = signUp ? signInHref : signUpHref;
|
|
394
|
+
const footerRow = <>
|
|
395
|
+
{linkHref && !pendingVerification ? <>
|
|
396
|
+
<Text className="font-instrument-sans text-sm" style={textStyle("muted", colorScheme)}>
|
|
397
|
+
{signUp ? "Already have an account?" : "Don't have an account?"}
|
|
398
|
+
</Text>
|
|
399
|
+
<Button testID={id(signUp ? "sign-in-link" : "sign-up-link")} variant="link" size="sm" fullWidth={false} onPress={() => router.push(linkHref)}>
|
|
400
|
+
{signUp ? "Sign in" : "Sign up"}
|
|
401
|
+
</Button>
|
|
402
|
+
</> : null}
|
|
403
|
+
{footer}
|
|
404
|
+
</>;
|
|
405
|
+
const hasFooter = Boolean(linkHref && !pendingVerification || footer);
|
|
406
|
+
if (pendingNewPassword) {
|
|
407
|
+
return <AuthScreen testID={id("new-password-screen")} title={AUTH_MESSAGES.newPasswordTitle} subtitle={AUTH_MESSAGES.newPasswordDescription} header={header}>
|
|
408
|
+
<PasswordInput testID={id("new-password")} accessibilityLabel={AUTH_MESSAGES.newPasswordLabel} autoCapitalize="none" autoComplete="new-password" textContentType="newPassword" autoFocus returnKeyType="next" submitBehavior="submit" onSubmitEditing={() => confirmField.current?.focus()} value={newPassword} placeholder={AUTH_MESSAGES.newPasswordLabel} onChangeText={setNewPassword} />
|
|
409
|
+
<PasswordInput testID={id("confirm-password")} ref={confirmField} accessibilityLabel={AUTH_MESSAGES.confirmPasswordLabel} autoCapitalize="none" autoComplete="new-password" textContentType="newPassword" returnKeyType="go" value={confirmPassword} placeholder={AUTH_MESSAGES.confirmPasswordLabel} onChangeText={setConfirmPassword} onSubmitEditing={() => void onNewPassword()} />
|
|
410
|
+
<FieldError testID={id("error")} message={error} />
|
|
411
|
+
<Button testID={id("new-password-submit")} loading={submitting} onPress={() => void onNewPassword()}>
|
|
412
|
+
{AUTH_MESSAGES.setPassword}
|
|
413
|
+
</Button>
|
|
414
|
+
<Button testID={id("new-password-back")} variant="ghost" onPress={onSecondFactorBack}>
|
|
415
|
+
Use a different account
|
|
416
|
+
</Button>
|
|
417
|
+
</AuthScreen>;
|
|
418
|
+
}
|
|
419
|
+
if (pendingFirstFactor && !pendingSecondFactor && !pendingNewPassword) {
|
|
420
|
+
const others = supportedFirstFactorStrategies(firstFactors).filter(strategy_3 => strategy_3 !== firstStrategy);
|
|
421
|
+
const switchTo = others[0] ?? null;
|
|
422
|
+
const copy = secondFactorCopy(firstStrategy, false);
|
|
423
|
+
return <SecondFactorScreen key={`first-${firstStrategy}`} title={copy.title} description={copy.description} footer={switchTo ? <Button testID={id("first-factor-switch")} variant="link" size="sm" fullWidth={false} disabled={firstLoading} onPress={() => void chooseFirstStrategy(switchTo, firstFactors)}>
|
|
424
|
+
{METHOD_LABEL[switchTo]}
|
|
425
|
+
</Button> : undefined} onSubmit={onFirstFactor} error={firstError} loading={firstLoading} onBack={onSecondFactorBack} backLabel="Use a different account" header={header} testIDPrefix={id("first-factor")} />;
|
|
426
|
+
}
|
|
427
|
+
if (pendingSecondFactor) {
|
|
428
|
+
const active = secondFactor.strategy;
|
|
429
|
+
const available = supportedSecondFactorStrategies(factors);
|
|
430
|
+
const others_0 = available.filter(strategy_4 => strategy_4 !== active);
|
|
431
|
+
// From a backup code, go back to the default non-backup method. Otherwise offer a backup code.
|
|
432
|
+
const switchTo_0 = active === "backup_code" ? pickSecondFactorStrategy(factors.filter(factor_1 => factor_1.strategy !== "backup_code")) : others_0.includes("backup_code") ? "backup_code" : null;
|
|
433
|
+
const copy_0 = secondFactorCopy(active, clientTrust);
|
|
434
|
+
const backup = active === "backup_code";
|
|
435
|
+
return <SecondFactorScreen key={active} title={copy_0.title} description={copy_0.description} codeLength={backup ? 8 : 6} digitsOnly={!backup} placeholder="Backup code" footer={switchTo_0 ? <Button testID={id("second-factor-switch")} variant="link" size="sm" fullWidth={false} disabled={secondFactor.loading} onPress={() => void chooseStrategy(switchTo_0, factors)}>
|
|
436
|
+
{METHOD_LABEL[switchTo_0]}
|
|
437
|
+
</Button> : undefined} onSubmit={onSecondFactor} error={secondFactor.error} loading={secondFactor.loading} onBack={onSecondFactorBack} backLabel="Use a different account" header={header} testIDPrefix={id("second-factor")} />;
|
|
438
|
+
}
|
|
439
|
+
if (pendingVerification) {
|
|
440
|
+
return <AuthScreen testID={id("screen")} title="Check your email" subtitle="Enter the code we sent to your email." header={header} footer={hasFooter ? footerRow : undefined}>
|
|
441
|
+
<Input testID={id("code")} accessibilityLabel="Verification code" value={code} placeholder="Verification code" keyboardType="number-pad" autoComplete="one-time-code" textContentType="oneTimeCode" autoFocus returnKeyType="go" onChangeText={setCode} onSubmitEditing={() => void onVerify()} />
|
|
442
|
+
<FieldError testID={id("error")} message={error} />
|
|
443
|
+
<Button testID={id("verify")} loading={submitting} onPress={() => void onVerify()}>
|
|
444
|
+
Verify
|
|
445
|
+
</Button>
|
|
446
|
+
<Button testID={id("back")} variant="ghost" onPress={() => {
|
|
447
|
+
setPendingVerification(false);
|
|
448
|
+
setCode("");
|
|
449
|
+
setError("");
|
|
450
|
+
}}>
|
|
451
|
+
Use a different email
|
|
452
|
+
</Button>
|
|
453
|
+
</AuthScreen>;
|
|
454
|
+
}
|
|
455
|
+
return <AuthScreen testID={id("screen")} title={title ?? (signUp ? "Create account" : "Welcome back")} subtitle={subtitle ?? (signUp ? "Sign up to get started" : "Sign in to continue")} header={header} footer={hasFooter ? footerRow : undefined}>
|
|
456
|
+
{showGoogle ? <>
|
|
457
|
+
<GoogleSignInButton testID={id("google")} scheme={scheme} redirectPath={redirectPath} afterAuthHref={afterAuthHref} replaceOptions={replaceOptions} onSuccess={onSuccess} onError={message_0 => fail(message_0, message_0)} label={googleLabel} disabled={submitting} />
|
|
458
|
+
<OrDivider />
|
|
459
|
+
</> : null}
|
|
460
|
+
<Input testID={id("email")} accessibilityLabel="Email" autoCapitalize="none" autoComplete="email" textContentType="emailAddress" keyboardType="email-address" returnKeyType="next" submitBehavior="submit" onSubmitEditing={() => passwordField.current?.focus()} value={email} placeholder="Email" onChangeText={setEmail} />
|
|
461
|
+
<PasswordInput testID={id("password")} ref={passwordField} accessibilityLabel="Password" autoCapitalize="none" autoComplete={signUp ? "new-password" : "password"} textContentType={signUp ? "newPassword" : "password"} returnKeyType="go" value={password} placeholder="Password" onChangeText={setPassword} onSubmitEditing={() => void (signUp ? onSignUp() : onSignIn())} />
|
|
462
|
+
<FieldError testID={id("error")} message={error} />
|
|
463
|
+
<Button testID={id("submit")} loading={submitting} onPress={() => void (signUp ? onSignUp() : onSignIn())}>
|
|
464
|
+
{signUp ? "Create account" : "Sign in"}
|
|
465
|
+
</Button>
|
|
466
|
+
{/* Bot protection mounts its widget here on web sign-up. */}
|
|
467
|
+
{signUp ? <View nativeID="clerk-captcha" /> : null}
|
|
468
|
+
</AuthScreen>;
|
|
469
|
+
}
|
|
470
|
+
/** `SignInScreen` with `mode="sign-up"`. */
|
|
471
|
+
export function SignUpScreen(props) {
|
|
472
|
+
const $ = _c(2);
|
|
473
|
+
let t0;
|
|
474
|
+
if ($[0] !== props) {
|
|
475
|
+
t0 = <SignInScreen {...props} mode="sign-up" />;
|
|
476
|
+
$[0] = props;
|
|
477
|
+
$[1] = t0;
|
|
478
|
+
} else {
|
|
479
|
+
t0 = $[1];
|
|
480
|
+
}
|
|
481
|
+
return t0;
|
|
482
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { isClerkAPIResponseError } from "@clerk/clerk-expo";
|
|
2
|
+
|
|
3
|
+
/** Words that must not reach the user: the auth provider and stack names. */
|
|
4
|
+
const PRODUCT_NAMES = /\b(clerk|convex)\b/gi;
|
|
5
|
+
|
|
6
|
+
/** The only Clerk codes that mean "Invalid email or password". */
|
|
7
|
+
export const CREDENTIAL_ERROR_CODES = ["form_password_incorrect", "form_identifier_not_found"];
|
|
8
|
+
const asString = value => typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9
|
+
|
|
10
|
+
/** Reads a Clerk API error, or a plain `{ errors: [...] }` object. No `zod`. */
|
|
11
|
+
function parseAuthError(error) {
|
|
12
|
+
if (typeof error !== "object" || error === null) return null;
|
|
13
|
+
const record = error;
|
|
14
|
+
if (!isClerkAPIResponseError(error) && !Array.isArray(record.errors)) return null;
|
|
15
|
+
const errors = Array.isArray(record.errors) ? record.errors.filter(item => typeof item === "object" && item !== null) : [];
|
|
16
|
+
return {
|
|
17
|
+
errors,
|
|
18
|
+
status: typeof record.status === "number" ? record.status : undefined,
|
|
19
|
+
clerkTraceId: asString(record.clerkTraceId),
|
|
20
|
+
errorName: asString(record.name)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function firstError(parsed) {
|
|
24
|
+
const first = parsed?.errors[0];
|
|
25
|
+
return {
|
|
26
|
+
code: asString(first?.code),
|
|
27
|
+
message: asString(first?.message),
|
|
28
|
+
longMessage: asString(first?.longMessage),
|
|
29
|
+
paramName: asString(first?.meta?.paramName)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The code of a timed-out auth stage. */
|
|
34
|
+
export const AUTH_TIMEOUT_CODE = "auth_timeout";
|
|
35
|
+
|
|
36
|
+
/** Default time for one Clerk call, in milliseconds. */
|
|
37
|
+
export const DEFAULT_STAGE_TIMEOUT_MS = 20000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The error `withAuthTimeout` rejects with. It is Clerk-shaped (`errors[0].code`
|
|
41
|
+
* is `auth_timeout`), so `authErrorMessage` and `logAuthFailure` read it.
|
|
42
|
+
*/
|
|
43
|
+
export class AuthTimeoutError extends Error {
|
|
44
|
+
code = AUTH_TIMEOUT_CODE;
|
|
45
|
+
constructor(stage, ms) {
|
|
46
|
+
const message = `The ${stage} step timed out after ${Math.round(ms / 1000)} seconds. Try again.`;
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "AuthTimeoutError";
|
|
49
|
+
this.stage = stage;
|
|
50
|
+
this.errors = [{
|
|
51
|
+
code: AUTH_TIMEOUT_CODE,
|
|
52
|
+
message
|
|
53
|
+
}];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function isAuthTimeoutError(error) {
|
|
57
|
+
return error instanceof AuthTimeoutError;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Rejects with `AuthTimeoutError` (code `auth_timeout`, message names `stage`)
|
|
62
|
+
* when `promise` has not settled after `ms`. The timer is cleared on settle.
|
|
63
|
+
*/
|
|
64
|
+
export function withAuthTimeout(promise, stage, ms = DEFAULT_STAGE_TIMEOUT_MS) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const timer = setTimeout(() => reject(new AuthTimeoutError(stage, ms)), ms);
|
|
67
|
+
promise.then(value => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
resolve(value);
|
|
70
|
+
}, error => {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
reject(error);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Logs a failed auth call as one structured `console.error`. It holds the
|
|
79
|
+
* Clerk code, message, longMessage and meta.paramName, plus stage, errorName,
|
|
80
|
+
* status and clerkTraceId when present. For `AuthTimeoutError` the logged stage is the error's own stage. Only the error object is read, so a
|
|
81
|
+
* password or one-time code is never logged.
|
|
82
|
+
*/
|
|
83
|
+
export function logAuthFailure(stage, error) {
|
|
84
|
+
if (isAuthTimeoutError(error)) stage = error.stage;
|
|
85
|
+
const parsed = parseAuthError(error);
|
|
86
|
+
const {
|
|
87
|
+
code,
|
|
88
|
+
message,
|
|
89
|
+
longMessage,
|
|
90
|
+
paramName
|
|
91
|
+
} = firstError(parsed);
|
|
92
|
+
const errorName = parsed?.errorName ?? (error instanceof Error ? error.name : undefined);
|
|
93
|
+
console.error("[sign-in] failure", {
|
|
94
|
+
stage,
|
|
95
|
+
...(errorName ? {
|
|
96
|
+
errorName
|
|
97
|
+
} : {}),
|
|
98
|
+
...(parsed?.status !== undefined ? {
|
|
99
|
+
status: parsed.status
|
|
100
|
+
} : {}),
|
|
101
|
+
...(parsed?.clerkTraceId ? {
|
|
102
|
+
clerkTraceId: parsed.clerkTraceId
|
|
103
|
+
} : {}),
|
|
104
|
+
code,
|
|
105
|
+
message,
|
|
106
|
+
longMessage,
|
|
107
|
+
meta: {
|
|
108
|
+
paramName
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A message to show for an auth error. For a Clerk error it gives the first
|
|
114
|
+
* `longMessage` or `message`, with `clerk` and `convex` written as "the
|
|
115
|
+
* sign-in service", then `(code: <code>)`. With no text it gives `fallback`,
|
|
116
|
+
* still followed by the code when there is one. With `credentials: true`, the
|
|
117
|
+
* two credential codes give `fallback` alone. Anything that is not a Clerk
|
|
118
|
+
* error gives `fallback`. Never shows a raw exception message.
|
|
119
|
+
*/
|
|
120
|
+
export function authErrorMessage(error, fallback, options = {}) {
|
|
121
|
+
const parsed = parseAuthError(error);
|
|
122
|
+
if (!parsed) return fallback;
|
|
123
|
+
const {
|
|
124
|
+
code,
|
|
125
|
+
message,
|
|
126
|
+
longMessage
|
|
127
|
+
} = firstError(parsed);
|
|
128
|
+
if (options.credentials && code && CREDENTIAL_ERROR_CODES.includes(code)) return fallback;
|
|
129
|
+
const text = (longMessage ?? message)?.replace(PRODUCT_NAMES, "the sign-in service") ?? fallback;
|
|
130
|
+
return code ? `${text} (code: ${code})` : text;
|
|
131
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Clerk + Convex providers and the shared sign-in and sign-up screens. On
|
|
2
|
+
// `@vvedantb/native-ui/auth` only: they import `@clerk/clerk-expo`, `convex`,
|
|
3
|
+
// `expo-auth-session`, `expo-web-browser` and `expo-router`, and Metro does not
|
|
4
|
+
// tree-shake.
|
|
5
|
+
export { AuthProviders } from "./AuthProviders";
|
|
6
|
+
export { AuthScreen, OrDivider, FieldError } from "./AuthScreen";
|
|
7
|
+
export { authErrorMessage, logAuthFailure, withAuthTimeout, AuthTimeoutError, isAuthTimeoutError, AUTH_TIMEOUT_CODE, DEFAULT_STAGE_TIMEOUT_MS, CREDENTIAL_ERROR_CODES } from "./authErrors";
|
|
8
|
+
export { GoogleSignInButton, useGoogleSignIn, useWarmUpBrowser, GOOGLE_ERROR_MESSAGE } from "./GoogleSignInButton";
|
|
9
|
+
export { SignInScreen, SignUpScreen, AUTH_MESSAGES } from "./SignInScreen";
|
|
10
|
+
export { SecondFactorScreen } from "./SecondFactorScreen";
|
|
11
|
+
export { submitTotpSecondFactor, submitSecondFactor, prepareSecondFactorIfNeeded, prepareFirstFactorIfNeeded, submitFirstFactor, pickFirstFactorStrategy, supportedFirstFactorStrategies, FIRST_FACTOR_PREFERENCE, pickSecondFactorStrategy, supportedSecondFactorStrategies, SECOND_FACTOR_PREFERENCE, useSecondFactor, TOTP_MESSAGES } from "./totp";
|