@vvedantb/native-ui 0.11.0 → 0.11.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vvedantb/native-ui",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
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",
@@ -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 { authErrorMessage, logAuthFailure } from "./authErrors";
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({ signIn, code: firstCode, strategy: firstStrategy });
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 signIn.resetPassword({ password: newPassword });
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 signIn.create({ identifier: email, password });
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);
@@ -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 { authErrorMessage, logAuthFailure, CREDENTIAL_ERROR_CODES } from "./authErrors";
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,
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 { authErrorMessage } from "./authErrors";
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 = {
@@ -76,25 +87,37 @@ export async function prepareSecondFactorIfNeeded(args: {
76
87
  signIn: Pick<SecondFactorSignIn, "supportedSecondFactors" | "prepareSecondFactor">;
77
88
  strategy: SecondFactorStrategy;
78
89
  factor?: SecondFactor;
90
+ /** Time for the Clerk call. Default 20000 ms. */
91
+ timeoutMs?: number;
79
92
  }): Promise<TotpResult> {
80
93
  const { signIn, strategy } = args;
94
+ const ms = args.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
81
95
  if (strategy !== "phone_code" && strategy !== "email_code") return { ok: true };
82
96
  if (!signIn.prepareSecondFactor) return { ok: false, error: TOTP_MESSAGES.sendFailed };
83
97
  const factor = args.factor ?? signIn.supportedSecondFactors?.find((item) => item.strategy === strategy);
84
98
  try {
85
99
  if (strategy === "phone_code") {
86
- await signIn.prepareSecondFactor({
87
- strategy,
88
- ...(factor?.phoneNumberId ? { phoneNumberId: factor.phoneNumberId } : {}),
89
- });
100
+ await withAuthTimeout(
101
+ signIn.prepareSecondFactor({
102
+ strategy,
103
+ ...(factor?.phoneNumberId ? { phoneNumberId: factor.phoneNumberId } : {}),
104
+ }),
105
+ "prepare-second-factor",
106
+ ms,
107
+ );
90
108
  } else {
91
- await signIn.prepareSecondFactor({
92
- strategy,
93
- ...(factor?.emailAddressId ? { emailAddressId: factor.emailAddressId } : {}),
94
- });
109
+ await withAuthTimeout(
110
+ signIn.prepareSecondFactor({
111
+ strategy,
112
+ ...(factor?.emailAddressId ? { emailAddressId: factor.emailAddressId } : {}),
113
+ }),
114
+ "prepare-second-factor",
115
+ ms,
116
+ );
95
117
  }
96
118
  return { ok: true };
97
119
  } catch (caught) {
120
+ logTimeout("prepare-second-factor", caught);
98
121
  return { ok: false, error: authErrorMessage(caught, TOTP_MESSAGES.sendFailed) };
99
122
  }
100
123
  }
@@ -111,15 +134,23 @@ export async function submitSecondFactor(args: {
111
134
  setActive: TotpSetActive;
112
135
  code: string;
113
136
  strategy: SecondFactorStrategy;
137
+ /** Time for each Clerk call. Default 20000 ms. */
138
+ timeoutMs?: number;
114
139
  }): Promise<TotpResult> {
140
+ const ms = args.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
115
141
  try {
116
- const attempt = await args.signIn.attemptSecondFactor({ strategy: args.strategy, code: args.code });
142
+ const attempt = await withAuthTimeout(
143
+ args.signIn.attemptSecondFactor({ strategy: args.strategy, code: args.code }),
144
+ "attempt-second-factor",
145
+ ms,
146
+ );
117
147
  if (attempt.status === "complete" && attempt.createdSessionId) {
118
- await args.setActive({ session: attempt.createdSessionId });
148
+ await withAuthTimeout(args.setActive({ session: attempt.createdSessionId }), "set-active", ms);
119
149
  return { ok: true };
120
150
  }
121
151
  return { ok: false, error: TOTP_MESSAGES.needsAnotherStep };
122
152
  } catch (caught) {
153
+ logTimeout("attempt-second-factor", caught);
123
154
  const fallback = args.strategy === "totp" ? TOTP_MESSAGES.invalidCode : TOTP_MESSAGES.invalidCodeGeneric;
124
155
  return { ok: false, error: authErrorMessage(caught, fallback) };
125
156
  }
@@ -187,20 +218,32 @@ export async function prepareFirstFactorIfNeeded(args: {
187
218
  signIn: Pick<FirstFactorSignIn, "supportedFirstFactors" | "prepareFirstFactor">;
188
219
  strategy: FirstFactorStrategy;
189
220
  factor?: FirstFactor;
221
+ /** Time for the Clerk call. Default 20000 ms. */
222
+ timeoutMs?: number;
190
223
  }): Promise<TotpResult> {
191
224
  const { signIn, strategy } = args;
225
+ const ms = args.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
192
226
  if (!signIn.prepareFirstFactor) return { ok: false, error: TOTP_MESSAGES.sendFailed };
193
227
  const factor = args.factor ?? signIn.supportedFirstFactors?.find((item) => item.strategy === strategy);
194
228
  try {
195
229
  if (strategy === "phone_code") {
196
230
  if (!factor?.phoneNumberId) return { ok: false, error: TOTP_MESSAGES.sendFailed };
197
- await signIn.prepareFirstFactor({ strategy, phoneNumberId: factor.phoneNumberId });
231
+ await withAuthTimeout(
232
+ signIn.prepareFirstFactor({ strategy, phoneNumberId: factor.phoneNumberId }),
233
+ "prepare-first-factor",
234
+ ms,
235
+ );
198
236
  } else {
199
237
  if (!factor?.emailAddressId) return { ok: false, error: TOTP_MESSAGES.sendFailed };
200
- await signIn.prepareFirstFactor({ strategy, emailAddressId: factor.emailAddressId });
238
+ await withAuthTimeout(
239
+ signIn.prepareFirstFactor({ strategy, emailAddressId: factor.emailAddressId }),
240
+ "prepare-first-factor",
241
+ ms,
242
+ );
201
243
  }
202
244
  return { ok: true };
203
245
  } catch (caught) {
246
+ logTimeout("prepare-first-factor", caught);
204
247
  return { ok: false, error: authErrorMessage(caught, TOTP_MESSAGES.sendFailed) };
205
248
  }
206
249
  }
@@ -214,11 +257,18 @@ export async function submitFirstFactor(args: {
214
257
  signIn: Pick<FirstFactorSignIn, "attemptFirstFactor">;
215
258
  code: string;
216
259
  strategy: FirstFactorStrategy;
260
+ /** Time for the Clerk call. Default 20000 ms. */
261
+ timeoutMs?: number;
217
262
  }): Promise<FirstFactorResult> {
218
263
  try {
219
- const attempt = await args.signIn.attemptFirstFactor({ strategy: args.strategy, code: args.code });
264
+ const attempt = await withAuthTimeout(
265
+ args.signIn.attemptFirstFactor({ strategy: args.strategy, code: args.code }),
266
+ "attempt-first-factor",
267
+ args.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS,
268
+ );
220
269
  return { ok: true, attempt };
221
270
  } catch (caught) {
271
+ logTimeout("attempt-first-factor", caught);
222
272
  return { ok: false, error: authErrorMessage(caught, TOTP_MESSAGES.invalidCodeGeneric) };
223
273
  }
224
274
  }
@@ -228,13 +278,22 @@ export function submitTotpSecondFactor(args: {
228
278
  signIn: TotpSignIn;
229
279
  setActive: TotpSetActive;
230
280
  code: string;
281
+ timeoutMs?: number;
231
282
  }): Promise<TotpResult> {
232
- return submitSecondFactor({ signIn: args.signIn, setActive: args.setActive, code: args.code, strategy: "totp" });
283
+ return submitSecondFactor({
284
+ signIn: args.signIn,
285
+ setActive: args.setActive,
286
+ code: args.code,
287
+ strategy: "totp",
288
+ timeoutMs: args.timeoutMs,
289
+ });
233
290
  }
234
291
 
235
292
  export type UseSecondFactorOptions = {
236
293
  /** The strategy `submit` uses until `setStrategy` changes it. Default `"totp"`. */
237
294
  strategy?: SecondFactorStrategy;
295
+ /** Time for each Clerk call, in milliseconds. Default 20000. */
296
+ stageTimeoutMs?: number;
238
297
  };
239
298
 
240
299
  export type UseSecondFactorResult = {
@@ -265,6 +324,7 @@ export function useSecondFactor(options: UseSecondFactorOptions = {}): UseSecond
265
324
  const strategy = chosen ?? options.strategy ?? "totp";
266
325
  const [loading, setLoading] = useState(false);
267
326
  const [error, setError] = useState("");
327
+ const timeoutMs = options.stageTimeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
268
328
 
269
329
  const submit = async (code: string): Promise<TotpResult> => {
270
330
  if (loading) return { ok: false, error };
@@ -274,7 +334,7 @@ export function useSecondFactor(options: UseSecondFactorOptions = {}): UseSecond
274
334
  }
275
335
  setLoading(true);
276
336
  setError("");
277
- const result = await submitSecondFactor({ signIn, setActive, code, strategy });
337
+ const result = await submitSecondFactor({ signIn, setActive, code, strategy, timeoutMs });
278
338
  if (!result.ok) setError(result.error);
279
339
  setLoading(false);
280
340
  return result;
@@ -290,7 +350,7 @@ export function useSecondFactor(options: UseSecondFactorOptions = {}): UseSecond
290
350
  }
291
351
  setLoading(true);
292
352
  setError("");
293
- const result = await prepareSecondFactorIfNeeded({ signIn, strategy: prepareStrategy, factor });
353
+ const result = await prepareSecondFactorIfNeeded({ signIn, strategy: prepareStrategy, factor, timeoutMs });
294
354
  if (!result.ok) setError(result.error);
295
355
  setLoading(false);
296
356
  return result;