@vvedantb/native-ui 0.11.0 → 0.11.2
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/package.json +4 -2
- package/src/auth/SignInScreen.tsx +36 -7
- package/src/auth/authErrors.ts +49 -1
- package/src/auth/index.ts +12 -1
- package/src/auth/totp.ts +97 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vvedantb/native-ui",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Shared React Native / Expo UI primitives. Floating surfaces, forms, overlays, toasts, settings chrome, tab bar, AI chat parts, the family app marks, a NativeWind preset, the Expo web root and Clerk + Convex auth screens.",
|
|
6
6
|
"type": "module",
|
|
@@ -214,6 +214,7 @@
|
|
|
214
214
|
}
|
|
215
215
|
},
|
|
216
216
|
"devDependencies": {
|
|
217
|
+
"@clerk/clerk-expo": "2.20.2",
|
|
217
218
|
"@types/react": "^19.1.0",
|
|
218
219
|
"react": "^19.1.0",
|
|
219
220
|
"typescript": "^5.9.0",
|
|
@@ -222,6 +223,7 @@
|
|
|
222
223
|
"scripts": {
|
|
223
224
|
"build": "node -e \"require('node:fs').accessSync('src/index.ts')\"",
|
|
224
225
|
"test": "vitest run",
|
|
225
|
-
"typecheck": "tsc -p tsconfig.typecheck.json"
|
|
226
|
+
"typecheck": "tsc -p tsconfig.typecheck.json && pnpm run typecheck:consumer",
|
|
227
|
+
"typecheck:consumer": "tsc -p tsconfig.consumer.json"
|
|
226
228
|
}
|
|
227
229
|
}
|
|
@@ -9,7 +9,13 @@ import { PasswordInput } from "../PasswordInput";
|
|
|
9
9
|
import { toast } from "../toastStore";
|
|
10
10
|
import { useScheme } from "../useScheme";
|
|
11
11
|
import { AuthScreen, FieldError, OrDivider } from "./AuthScreen";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
authErrorMessage,
|
|
14
|
+
DEFAULT_STAGE_TIMEOUT_MS,
|
|
15
|
+
isAuthTimeoutError,
|
|
16
|
+
logAuthFailure,
|
|
17
|
+
withAuthTimeout,
|
|
18
|
+
} from "./authErrors";
|
|
13
19
|
import { SecondFactorScreen } from "./SecondFactorScreen";
|
|
14
20
|
import {
|
|
15
21
|
pickFirstFactorStrategy,
|
|
@@ -139,6 +145,12 @@ export type SignInScreenProps = AuthNavigationOptions & {
|
|
|
139
145
|
* `console.warn`. Ignored in `sign-up`.
|
|
140
146
|
*/
|
|
141
147
|
onIncomplete?: (signIn: IncompleteSignIn) => void;
|
|
148
|
+
/**
|
|
149
|
+
* Time for each Clerk call (create, prepare, attempt, reset password,
|
|
150
|
+
* set active), in milliseconds. A call that takes longer fails with code
|
|
151
|
+
* `auth_timeout`. Default 20000.
|
|
152
|
+
*/
|
|
153
|
+
stageTimeoutMs?: number;
|
|
142
154
|
};
|
|
143
155
|
|
|
144
156
|
/**
|
|
@@ -167,6 +179,7 @@ export function SignInScreen({
|
|
|
167
179
|
header,
|
|
168
180
|
footer,
|
|
169
181
|
onIncomplete,
|
|
182
|
+
stageTimeoutMs = DEFAULT_STAGE_TIMEOUT_MS,
|
|
170
183
|
}: SignInScreenProps) {
|
|
171
184
|
const signInState = useSignIn();
|
|
172
185
|
const signUpState = useSignUp();
|
|
@@ -187,7 +200,7 @@ export function SignInScreen({
|
|
|
187
200
|
const [pendingNewPassword, setPendingNewPassword] = useState(false);
|
|
188
201
|
const [newPassword, setNewPassword] = useState("");
|
|
189
202
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
190
|
-
const secondFactor = useSecondFactor();
|
|
203
|
+
const secondFactor = useSecondFactor({ stageTimeoutMs });
|
|
191
204
|
const [error, setError] = useState("");
|
|
192
205
|
const [submitting, setSubmitting] = useState(false);
|
|
193
206
|
const signUp = mode === "sign-up";
|
|
@@ -249,6 +262,7 @@ export function SignInScreen({
|
|
|
249
262
|
signIn,
|
|
250
263
|
strategy,
|
|
251
264
|
factor: list.find((factor) => factor.strategy === strategy),
|
|
265
|
+
timeoutMs: stageTimeoutMs,
|
|
252
266
|
});
|
|
253
267
|
if (!sent.ok) setFirstError(sent.error);
|
|
254
268
|
setFirstLoading(false);
|
|
@@ -268,7 +282,7 @@ export function SignInScreen({
|
|
|
268
282
|
// Clerk's `SignInStatus` omits `needs_client_trust`: widen before comparing.
|
|
269
283
|
const status: string | null = attempt.status;
|
|
270
284
|
if (status === "complete" && attempt.createdSessionId && setActive) {
|
|
271
|
-
await setActive({ session: attempt.createdSessionId });
|
|
285
|
+
await withAuthTimeout(setActive({ session: attempt.createdSessionId }), "set-active", stageTimeoutMs);
|
|
272
286
|
done();
|
|
273
287
|
} else if (status === "needs_new_password") {
|
|
274
288
|
setPendingFirstFactor(false);
|
|
@@ -288,7 +302,12 @@ export function SignInScreen({
|
|
|
288
302
|
if (!isLoaded || !signIn || firstLoading) return;
|
|
289
303
|
setFirstLoading(true);
|
|
290
304
|
setFirstError("");
|
|
291
|
-
const result = await submitFirstFactor({
|
|
305
|
+
const result = await submitFirstFactor({
|
|
306
|
+
signIn,
|
|
307
|
+
code: firstCode,
|
|
308
|
+
strategy: firstStrategy,
|
|
309
|
+
timeoutMs: stageTimeoutMs,
|
|
310
|
+
});
|
|
292
311
|
if (!result.ok) {
|
|
293
312
|
setFirstError(result.error);
|
|
294
313
|
setFirstLoading(false);
|
|
@@ -297,6 +316,7 @@ export function SignInScreen({
|
|
|
297
316
|
try {
|
|
298
317
|
await routeAttempt(result.attempt, false);
|
|
299
318
|
} catch (caught) {
|
|
319
|
+
if (isAuthTimeoutError(caught)) logAuthFailure("set-active", caught);
|
|
300
320
|
setFirstError(authErrorMessage(caught, AUTH_MESSAGES.verifyFailed));
|
|
301
321
|
} finally {
|
|
302
322
|
setFirstLoading(false);
|
|
@@ -316,9 +336,13 @@ export function SignInScreen({
|
|
|
316
336
|
setSubmitting(true);
|
|
317
337
|
setError("");
|
|
318
338
|
try {
|
|
319
|
-
const result = await
|
|
339
|
+
const result = await withAuthTimeout(
|
|
340
|
+
signIn.resetPassword({ password: newPassword }),
|
|
341
|
+
"reset-password",
|
|
342
|
+
stageTimeoutMs,
|
|
343
|
+
);
|
|
320
344
|
if (result.status === "complete" && result.createdSessionId) {
|
|
321
|
-
await setActive({ session: result.createdSessionId });
|
|
345
|
+
await withAuthTimeout(setActive({ session: result.createdSessionId }), "set-active", stageTimeoutMs);
|
|
322
346
|
done();
|
|
323
347
|
} else if (onIncomplete) {
|
|
324
348
|
onIncomplete(result);
|
|
@@ -327,6 +351,7 @@ export function SignInScreen({
|
|
|
327
351
|
fail(AUTH_MESSAGES.resetPasswordFailed, AUTH_MESSAGES.needsAnotherStep);
|
|
328
352
|
}
|
|
329
353
|
} catch (caught) {
|
|
354
|
+
if (isAuthTimeoutError(caught)) logAuthFailure("reset-password", caught);
|
|
330
355
|
fail(AUTH_MESSAGES.resetPasswordFailed, authErrorMessage(caught, AUTH_MESSAGES.resetPasswordFailed));
|
|
331
356
|
} finally {
|
|
332
357
|
setSubmitting(false);
|
|
@@ -340,7 +365,11 @@ export function SignInScreen({
|
|
|
340
365
|
setSubmitting(true);
|
|
341
366
|
setError("");
|
|
342
367
|
try {
|
|
343
|
-
const attempt = await
|
|
368
|
+
const attempt = await withAuthTimeout(
|
|
369
|
+
signIn.create({ identifier: email, password }),
|
|
370
|
+
"create",
|
|
371
|
+
stageTimeoutMs,
|
|
372
|
+
);
|
|
344
373
|
await routeAttempt(attempt, true);
|
|
345
374
|
} catch (caught) {
|
|
346
375
|
logAuthFailure("create", caught);
|
package/src/auth/authErrors.ts
CHANGED
|
@@ -49,13 +49,61 @@ function firstError(parsed: ParsedAuthError | null) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** The code of a timed-out auth stage. */
|
|
53
|
+
export const AUTH_TIMEOUT_CODE = "auth_timeout";
|
|
54
|
+
|
|
55
|
+
/** Default time for one Clerk call, in milliseconds. */
|
|
56
|
+
export const DEFAULT_STAGE_TIMEOUT_MS = 20000;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The error `withAuthTimeout` rejects with. It is Clerk-shaped (`errors[0].code`
|
|
60
|
+
* is `auth_timeout`), so `authErrorMessage` and `logAuthFailure` read it.
|
|
61
|
+
*/
|
|
62
|
+
export class AuthTimeoutError extends Error {
|
|
63
|
+
readonly code = AUTH_TIMEOUT_CODE;
|
|
64
|
+
readonly stage: string;
|
|
65
|
+
readonly errors: { code: string; message: string }[];
|
|
66
|
+
constructor(stage: string, ms: number) {
|
|
67
|
+
const message = `The ${stage} step timed out after ${Math.round(ms / 1000)} seconds. Try again.`;
|
|
68
|
+
super(message);
|
|
69
|
+
this.name = "AuthTimeoutError";
|
|
70
|
+
this.stage = stage;
|
|
71
|
+
this.errors = [{ code: AUTH_TIMEOUT_CODE, message }];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isAuthTimeoutError(error: unknown): error is AuthTimeoutError {
|
|
76
|
+
return error instanceof AuthTimeoutError;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Rejects with `AuthTimeoutError` (code `auth_timeout`, message names `stage`)
|
|
81
|
+
* when `promise` has not settled after `ms`. The timer is cleared on settle.
|
|
82
|
+
*/
|
|
83
|
+
export function withAuthTimeout<T>(promise: Promise<T>, stage: string, ms: number = DEFAULT_STAGE_TIMEOUT_MS): Promise<T> {
|
|
84
|
+
return new Promise<T>((resolve, reject) => {
|
|
85
|
+
const timer = setTimeout(() => reject(new AuthTimeoutError(stage, ms)), ms);
|
|
86
|
+
promise.then(
|
|
87
|
+
(value) => {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
resolve(value);
|
|
90
|
+
},
|
|
91
|
+
(error) => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
reject(error);
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
52
99
|
/**
|
|
53
100
|
* Logs a failed auth call as one structured `console.error`. It holds the
|
|
54
101
|
* Clerk code, message, longMessage and meta.paramName, plus stage, errorName,
|
|
55
|
-
* status and clerkTraceId when present. Only the error object is read, so a
|
|
102
|
+
* status and clerkTraceId when present. For `AuthTimeoutError` the logged stage is the error's own stage. Only the error object is read, so a
|
|
56
103
|
* password or one-time code is never logged.
|
|
57
104
|
*/
|
|
58
105
|
export function logAuthFailure(stage: string, error: unknown): void {
|
|
106
|
+
if (isAuthTimeoutError(error)) stage = error.stage;
|
|
59
107
|
const parsed = parseAuthError(error);
|
|
60
108
|
const { code, message, longMessage, paramName } = firstError(parsed);
|
|
61
109
|
const errorName = parsed?.errorName ?? (error instanceof Error ? error.name : undefined);
|
package/src/auth/index.ts
CHANGED
|
@@ -6,7 +6,16 @@ export { AuthProviders } from "./AuthProviders";
|
|
|
6
6
|
export type { AuthProvidersProps } from "./AuthProviders";
|
|
7
7
|
export { AuthScreen, OrDivider, FieldError } from "./AuthScreen";
|
|
8
8
|
export type { AuthScreenProps } from "./AuthScreen";
|
|
9
|
-
export {
|
|
9
|
+
export {
|
|
10
|
+
authErrorMessage,
|
|
11
|
+
logAuthFailure,
|
|
12
|
+
withAuthTimeout,
|
|
13
|
+
AuthTimeoutError,
|
|
14
|
+
isAuthTimeoutError,
|
|
15
|
+
AUTH_TIMEOUT_CODE,
|
|
16
|
+
DEFAULT_STAGE_TIMEOUT_MS,
|
|
17
|
+
CREDENTIAL_ERROR_CODES,
|
|
18
|
+
} from "./authErrors";
|
|
10
19
|
export type { AuthErrorOptions } from "./authErrors";
|
|
11
20
|
export {
|
|
12
21
|
GoogleSignInButton,
|
|
@@ -43,6 +52,8 @@ export type {
|
|
|
43
52
|
FirstFactorResult,
|
|
44
53
|
FirstFactorSignIn,
|
|
45
54
|
FirstFactorStrategy,
|
|
55
|
+
PrepareFirstFactorParams,
|
|
56
|
+
PrepareSecondFactorParams,
|
|
46
57
|
SecondFactor,
|
|
47
58
|
SecondFactorSignIn,
|
|
48
59
|
SecondFactorStrategy,
|
package/src/auth/totp.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { useSignIn } from "@clerk/clerk-expo";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
authErrorMessage,
|
|
5
|
+
DEFAULT_STAGE_TIMEOUT_MS,
|
|
6
|
+
isAuthTimeoutError,
|
|
7
|
+
logAuthFailure,
|
|
8
|
+
withAuthTimeout,
|
|
9
|
+
} from "./authErrors";
|
|
10
|
+
|
|
11
|
+
/** Logs a timed-out stage. Other failures are not logged here. */
|
|
12
|
+
const logTimeout = (stage: string, error: unknown) => {
|
|
13
|
+
if (isAuthTimeoutError(error)) logAuthFailure(stage, error);
|
|
14
|
+
};
|
|
4
15
|
|
|
5
16
|
/** The messages the second-factor step can show. Generic: none names a provider. */
|
|
6
17
|
export const TOTP_MESSAGES = {
|
|
@@ -32,6 +43,24 @@ export type SecondFactor = {
|
|
|
32
43
|
|
|
33
44
|
type AttemptResult = { status: string | null; createdSessionId: string | null };
|
|
34
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The params of `prepareSecondFactor`. A union on `strategy`, as in Clerk's
|
|
48
|
+
* `PhoneCodeSecondFactorConfig | EmailCodeSecondFactorConfig`. One object with
|
|
49
|
+
* `strategy: "phone_code" | "email_code"` does not accept Clerk's
|
|
50
|
+
* `SignInResource` under `strictFunctionTypes`.
|
|
51
|
+
*/
|
|
52
|
+
export type PrepareSecondFactorParams =
|
|
53
|
+
| { strategy: "phone_code"; phoneNumberId?: string }
|
|
54
|
+
| { strategy: "email_code"; emailAddressId?: string };
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The params of `prepareFirstFactor`. A union on `strategy`, as in Clerk's
|
|
58
|
+
* `EmailCodeConfig | PhoneCodeConfig`, where the id is required.
|
|
59
|
+
*/
|
|
60
|
+
export type PrepareFirstFactorParams =
|
|
61
|
+
| { strategy: "email_code"; emailAddressId: string }
|
|
62
|
+
| { strategy: "phone_code"; phoneNumberId: string };
|
|
63
|
+
|
|
35
64
|
/** The part of the sign-in resource that TOTP needs. Tests pass a mock. */
|
|
36
65
|
export type TotpSignIn = {
|
|
37
66
|
attemptSecondFactor(params: { strategy: "totp"; code: string }): Promise<AttemptResult>;
|
|
@@ -40,11 +69,7 @@ export type TotpSignIn = {
|
|
|
40
69
|
/** The part of the sign-in resource that any second factor needs. Tests pass a mock. */
|
|
41
70
|
export type SecondFactorSignIn = {
|
|
42
71
|
supportedSecondFactors?: SecondFactor[] | null;
|
|
43
|
-
prepareSecondFactor?(params:
|
|
44
|
-
strategy: "phone_code" | "email_code";
|
|
45
|
-
phoneNumberId?: string;
|
|
46
|
-
emailAddressId?: string;
|
|
47
|
-
}): Promise<unknown>;
|
|
72
|
+
prepareSecondFactor?(params: PrepareSecondFactorParams): Promise<unknown>;
|
|
48
73
|
attemptSecondFactor(params: { strategy: SecondFactorStrategy; code: string }): Promise<AttemptResult>;
|
|
49
74
|
};
|
|
50
75
|
|
|
@@ -76,25 +101,37 @@ export async function prepareSecondFactorIfNeeded(args: {
|
|
|
76
101
|
signIn: Pick<SecondFactorSignIn, "supportedSecondFactors" | "prepareSecondFactor">;
|
|
77
102
|
strategy: SecondFactorStrategy;
|
|
78
103
|
factor?: SecondFactor;
|
|
104
|
+
/** Time for the Clerk call. Default 20000 ms. */
|
|
105
|
+
timeoutMs?: number;
|
|
79
106
|
}): Promise<TotpResult> {
|
|
80
107
|
const { signIn, strategy } = args;
|
|
108
|
+
const ms = args.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
|
|
81
109
|
if (strategy !== "phone_code" && strategy !== "email_code") return { ok: true };
|
|
82
110
|
if (!signIn.prepareSecondFactor) return { ok: false, error: TOTP_MESSAGES.sendFailed };
|
|
83
111
|
const factor = args.factor ?? signIn.supportedSecondFactors?.find((item) => item.strategy === strategy);
|
|
84
112
|
try {
|
|
85
113
|
if (strategy === "phone_code") {
|
|
86
|
-
await
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
114
|
+
await withAuthTimeout(
|
|
115
|
+
signIn.prepareSecondFactor({
|
|
116
|
+
strategy,
|
|
117
|
+
...(factor?.phoneNumberId ? { phoneNumberId: factor.phoneNumberId } : {}),
|
|
118
|
+
}),
|
|
119
|
+
"prepare-second-factor",
|
|
120
|
+
ms,
|
|
121
|
+
);
|
|
90
122
|
} else {
|
|
91
|
-
await
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
123
|
+
await withAuthTimeout(
|
|
124
|
+
signIn.prepareSecondFactor({
|
|
125
|
+
strategy,
|
|
126
|
+
...(factor?.emailAddressId ? { emailAddressId: factor.emailAddressId } : {}),
|
|
127
|
+
}),
|
|
128
|
+
"prepare-second-factor",
|
|
129
|
+
ms,
|
|
130
|
+
);
|
|
95
131
|
}
|
|
96
132
|
return { ok: true };
|
|
97
133
|
} catch (caught) {
|
|
134
|
+
logTimeout("prepare-second-factor", caught);
|
|
98
135
|
return { ok: false, error: authErrorMessage(caught, TOTP_MESSAGES.sendFailed) };
|
|
99
136
|
}
|
|
100
137
|
}
|
|
@@ -111,15 +148,23 @@ export async function submitSecondFactor(args: {
|
|
|
111
148
|
setActive: TotpSetActive;
|
|
112
149
|
code: string;
|
|
113
150
|
strategy: SecondFactorStrategy;
|
|
151
|
+
/** Time for each Clerk call. Default 20000 ms. */
|
|
152
|
+
timeoutMs?: number;
|
|
114
153
|
}): Promise<TotpResult> {
|
|
154
|
+
const ms = args.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
|
|
115
155
|
try {
|
|
116
|
-
const attempt = await
|
|
156
|
+
const attempt = await withAuthTimeout(
|
|
157
|
+
args.signIn.attemptSecondFactor({ strategy: args.strategy, code: args.code }),
|
|
158
|
+
"attempt-second-factor",
|
|
159
|
+
ms,
|
|
160
|
+
);
|
|
117
161
|
if (attempt.status === "complete" && attempt.createdSessionId) {
|
|
118
|
-
await args.setActive({ session: attempt.createdSessionId });
|
|
162
|
+
await withAuthTimeout(args.setActive({ session: attempt.createdSessionId }), "set-active", ms);
|
|
119
163
|
return { ok: true };
|
|
120
164
|
}
|
|
121
165
|
return { ok: false, error: TOTP_MESSAGES.needsAnotherStep };
|
|
122
166
|
} catch (caught) {
|
|
167
|
+
logTimeout("attempt-second-factor", caught);
|
|
123
168
|
const fallback = args.strategy === "totp" ? TOTP_MESSAGES.invalidCode : TOTP_MESSAGES.invalidCodeGeneric;
|
|
124
169
|
return { ok: false, error: authErrorMessage(caught, fallback) };
|
|
125
170
|
}
|
|
@@ -142,11 +187,7 @@ export type FirstFactor = {
|
|
|
142
187
|
/** The part of the sign-in resource that a first-factor code needs. Tests pass a mock. */
|
|
143
188
|
export type FirstFactorSignIn = {
|
|
144
189
|
supportedFirstFactors?: FirstFactor[] | null;
|
|
145
|
-
prepareFirstFactor?(params:
|
|
146
|
-
strategy: FirstFactorStrategy;
|
|
147
|
-
phoneNumberId?: string;
|
|
148
|
-
emailAddressId?: string;
|
|
149
|
-
}): Promise<unknown>;
|
|
190
|
+
prepareFirstFactor?(params: PrepareFirstFactorParams): Promise<unknown>;
|
|
150
191
|
attemptFirstFactor(params: { strategy: FirstFactorStrategy; code: string }): Promise<AttemptResult>;
|
|
151
192
|
};
|
|
152
193
|
|
|
@@ -187,20 +228,32 @@ export async function prepareFirstFactorIfNeeded(args: {
|
|
|
187
228
|
signIn: Pick<FirstFactorSignIn, "supportedFirstFactors" | "prepareFirstFactor">;
|
|
188
229
|
strategy: FirstFactorStrategy;
|
|
189
230
|
factor?: FirstFactor;
|
|
231
|
+
/** Time for the Clerk call. Default 20000 ms. */
|
|
232
|
+
timeoutMs?: number;
|
|
190
233
|
}): Promise<TotpResult> {
|
|
191
234
|
const { signIn, strategy } = args;
|
|
235
|
+
const ms = args.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
|
|
192
236
|
if (!signIn.prepareFirstFactor) return { ok: false, error: TOTP_MESSAGES.sendFailed };
|
|
193
237
|
const factor = args.factor ?? signIn.supportedFirstFactors?.find((item) => item.strategy === strategy);
|
|
194
238
|
try {
|
|
195
239
|
if (strategy === "phone_code") {
|
|
196
240
|
if (!factor?.phoneNumberId) return { ok: false, error: TOTP_MESSAGES.sendFailed };
|
|
197
|
-
await
|
|
241
|
+
await withAuthTimeout(
|
|
242
|
+
signIn.prepareFirstFactor({ strategy, phoneNumberId: factor.phoneNumberId }),
|
|
243
|
+
"prepare-first-factor",
|
|
244
|
+
ms,
|
|
245
|
+
);
|
|
198
246
|
} else {
|
|
199
247
|
if (!factor?.emailAddressId) return { ok: false, error: TOTP_MESSAGES.sendFailed };
|
|
200
|
-
await
|
|
248
|
+
await withAuthTimeout(
|
|
249
|
+
signIn.prepareFirstFactor({ strategy, emailAddressId: factor.emailAddressId }),
|
|
250
|
+
"prepare-first-factor",
|
|
251
|
+
ms,
|
|
252
|
+
);
|
|
201
253
|
}
|
|
202
254
|
return { ok: true };
|
|
203
255
|
} catch (caught) {
|
|
256
|
+
logTimeout("prepare-first-factor", caught);
|
|
204
257
|
return { ok: false, error: authErrorMessage(caught, TOTP_MESSAGES.sendFailed) };
|
|
205
258
|
}
|
|
206
259
|
}
|
|
@@ -214,11 +267,18 @@ export async function submitFirstFactor(args: {
|
|
|
214
267
|
signIn: Pick<FirstFactorSignIn, "attemptFirstFactor">;
|
|
215
268
|
code: string;
|
|
216
269
|
strategy: FirstFactorStrategy;
|
|
270
|
+
/** Time for the Clerk call. Default 20000 ms. */
|
|
271
|
+
timeoutMs?: number;
|
|
217
272
|
}): Promise<FirstFactorResult> {
|
|
218
273
|
try {
|
|
219
|
-
const attempt = await
|
|
274
|
+
const attempt = await withAuthTimeout(
|
|
275
|
+
args.signIn.attemptFirstFactor({ strategy: args.strategy, code: args.code }),
|
|
276
|
+
"attempt-first-factor",
|
|
277
|
+
args.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS,
|
|
278
|
+
);
|
|
220
279
|
return { ok: true, attempt };
|
|
221
280
|
} catch (caught) {
|
|
281
|
+
logTimeout("attempt-first-factor", caught);
|
|
222
282
|
return { ok: false, error: authErrorMessage(caught, TOTP_MESSAGES.invalidCodeGeneric) };
|
|
223
283
|
}
|
|
224
284
|
}
|
|
@@ -228,13 +288,22 @@ export function submitTotpSecondFactor(args: {
|
|
|
228
288
|
signIn: TotpSignIn;
|
|
229
289
|
setActive: TotpSetActive;
|
|
230
290
|
code: string;
|
|
291
|
+
timeoutMs?: number;
|
|
231
292
|
}): Promise<TotpResult> {
|
|
232
|
-
return submitSecondFactor({
|
|
293
|
+
return submitSecondFactor({
|
|
294
|
+
signIn: args.signIn,
|
|
295
|
+
setActive: args.setActive,
|
|
296
|
+
code: args.code,
|
|
297
|
+
strategy: "totp",
|
|
298
|
+
timeoutMs: args.timeoutMs,
|
|
299
|
+
});
|
|
233
300
|
}
|
|
234
301
|
|
|
235
302
|
export type UseSecondFactorOptions = {
|
|
236
303
|
/** The strategy `submit` uses until `setStrategy` changes it. Default `"totp"`. */
|
|
237
304
|
strategy?: SecondFactorStrategy;
|
|
305
|
+
/** Time for each Clerk call, in milliseconds. Default 20000. */
|
|
306
|
+
stageTimeoutMs?: number;
|
|
238
307
|
};
|
|
239
308
|
|
|
240
309
|
export type UseSecondFactorResult = {
|
|
@@ -265,6 +334,7 @@ export function useSecondFactor(options: UseSecondFactorOptions = {}): UseSecond
|
|
|
265
334
|
const strategy = chosen ?? options.strategy ?? "totp";
|
|
266
335
|
const [loading, setLoading] = useState(false);
|
|
267
336
|
const [error, setError] = useState("");
|
|
337
|
+
const timeoutMs = options.stageTimeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
|
|
268
338
|
|
|
269
339
|
const submit = async (code: string): Promise<TotpResult> => {
|
|
270
340
|
if (loading) return { ok: false, error };
|
|
@@ -274,7 +344,7 @@ export function useSecondFactor(options: UseSecondFactorOptions = {}): UseSecond
|
|
|
274
344
|
}
|
|
275
345
|
setLoading(true);
|
|
276
346
|
setError("");
|
|
277
|
-
const result = await submitSecondFactor({ signIn, setActive, code, strategy });
|
|
347
|
+
const result = await submitSecondFactor({ signIn, setActive, code, strategy, timeoutMs });
|
|
278
348
|
if (!result.ok) setError(result.error);
|
|
279
349
|
setLoading(false);
|
|
280
350
|
return result;
|
|
@@ -290,7 +360,7 @@ export function useSecondFactor(options: UseSecondFactorOptions = {}): UseSecond
|
|
|
290
360
|
}
|
|
291
361
|
setLoading(true);
|
|
292
362
|
setError("");
|
|
293
|
-
const result = await prepareSecondFactorIfNeeded({ signIn, strategy: prepareStrategy, factor });
|
|
363
|
+
const result = await prepareSecondFactorIfNeeded({ signIn, strategy: prepareStrategy, factor, timeoutMs });
|
|
294
364
|
if (!result.ok) setError(result.error);
|
|
295
365
|
setLoading(false);
|
|
296
366
|
return result;
|