@dloizides/auth-web 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/dist/index.mjs ADDED
@@ -0,0 +1,1373 @@
1
+ import { createContext, useState, useRef, useEffect, useCallback, useMemo, useContext } from 'react';
2
+ import { View, Text, TextInput, TouchableOpacity, ActivityIndicator, StyleSheet } from 'react-native';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+ import { useMutation } from '@tanstack/react-query';
5
+ import { BffAuthClient, createFetchHttpClient } from '@dloizides/auth-client';
6
+ export { BffAuthClient, createFetchHttpClient } from '@dloizides/auth-client';
7
+
8
+ // src/components/LoginForm.tsx
9
+
10
+ // src/components/labels.ts
11
+ var DEFAULT_LOGIN_LABELS = {
12
+ title: "Sign in",
13
+ subtitle: "Welcome back",
14
+ usernameLabel: "Username",
15
+ usernamePlaceholder: "Enter your username",
16
+ passwordLabel: "Password",
17
+ passwordPlaceholder: "Enter your password",
18
+ submit: "Sign in",
19
+ submitting: "Signing in...",
20
+ forgotPassword: "Forgot password?",
21
+ invalidCredentials: "Incorrect username or password.",
22
+ missingFields: "Enter both your username and password."
23
+ };
24
+ var DEFAULT_FORGOT_PASSWORD_LABELS = {
25
+ title: "Forgot password",
26
+ description: "Enter your email and we will send you a reset link.",
27
+ emailLabel: "Email",
28
+ emailPlaceholder: "Enter your email",
29
+ submit: "Send reset link",
30
+ submitting: "Sending...",
31
+ successMessage: "If that email is registered, a reset link is on its way.",
32
+ networkError: "Something went wrong. Please try again.",
33
+ invalidEmail: "Enter a valid email address."
34
+ };
35
+ var DEFAULT_OTP_LABELS = {
36
+ requestTitle: "Sign in with a code",
37
+ requestDescription: "Enter your email and we will send you a one-time code.",
38
+ emailLabel: "Email",
39
+ emailPlaceholder: "Enter your email",
40
+ requestSubmit: "Send code",
41
+ requesting: "Sending...",
42
+ invalidEmail: "Enter a valid email address.",
43
+ verifyTitle: "Enter your code",
44
+ verifyDescription: "We sent a one-time code to {identifier}.",
45
+ codeLabel: "One-time code",
46
+ codePlaceholder: "Enter the code",
47
+ verifySubmit: "Verify",
48
+ verifying: "Verifying...",
49
+ missingCode: "Enter the code we emailed you.",
50
+ invalidCode: "That code is incorrect or has expired.",
51
+ resend: "Resend code",
52
+ resending: "Resending...",
53
+ changeEmail: "Use a different email"
54
+ };
55
+ var DEFAULT_PIN_LABELS = {
56
+ title: "Enter your event PIN",
57
+ description: "Enter the PIN for this event to sign in.",
58
+ pinLabel: "PIN",
59
+ pinPlaceholder: "Enter your PIN",
60
+ submit: "Sign in",
61
+ submitting: "Signing in...",
62
+ missingPin: "Enter your event PIN.",
63
+ invalidPin: "That PIN is incorrect, has expired, or is locked out."
64
+ };
65
+ var DEFAULT_RESET_PASSWORD_LABELS = {
66
+ title: "Reset password",
67
+ description: "Choose a new password for your account.",
68
+ newPasswordLabel: "New password",
69
+ newPasswordPlaceholder: "Enter a new password",
70
+ confirmPasswordLabel: "Confirm password",
71
+ confirmPasswordPlaceholder: "Re-enter the new password",
72
+ submit: "Reset password",
73
+ submitting: "Resetting...",
74
+ errorEmpty: "Fill in both password fields.",
75
+ errorWeakPassword: "Use at least 8 characters with an uppercase letter, a lowercase letter and a digit.",
76
+ errorMismatch: "The passwords do not match.",
77
+ errorTokenInvalid: "This reset link is invalid or has expired. Request a new one.",
78
+ errorNetwork: "Something went wrong. Please try again."
79
+ };
80
+
81
+ // src/components/testIds.ts
82
+ var AuthTestIds = {
83
+ loginForm: "auth-login-form",
84
+ loginUsernameInput: "auth-login-username",
85
+ loginPasswordInput: "auth-login-password",
86
+ loginSubmitButton: "auth-login-submit",
87
+ loginForgotLink: "auth-login-forgot-link",
88
+ loginError: "auth-login-error",
89
+ forgotPasswordForm: "auth-forgot-form",
90
+ forgotPasswordEmailInput: "auth-forgot-email",
91
+ forgotPasswordSubmitButton: "auth-forgot-submit",
92
+ forgotPasswordError: "auth-forgot-error",
93
+ forgotPasswordSuccess: "auth-forgot-success",
94
+ resetPasswordForm: "auth-reset-form",
95
+ resetPasswordNewInput: "auth-reset-new",
96
+ resetPasswordConfirmInput: "auth-reset-confirm",
97
+ resetPasswordSubmitButton: "auth-reset-submit",
98
+ resetPasswordError: "auth-reset-error",
99
+ otpForm: "auth-otp-form",
100
+ otpEmailInput: "auth-otp-email",
101
+ otpRequestButton: "auth-otp-request",
102
+ otpCodeInput: "auth-otp-code",
103
+ otpVerifyButton: "auth-otp-verify",
104
+ otpResendButton: "auth-otp-resend",
105
+ otpChangeEmailButton: "auth-otp-change-email",
106
+ otpError: "auth-otp-error",
107
+ pinForm: "auth-pin-form",
108
+ pinInput: "auth-pin-input",
109
+ pinSubmitButton: "auth-pin-submit",
110
+ pinError: "auth-pin-error"
111
+ };
112
+ function withTestIdPrefix(baseId, prefix) {
113
+ return prefix === void 0 || prefix === "" ? baseId : `${prefix}-${baseId}`;
114
+ }
115
+ var FULL_WIDTH = "100%";
116
+ var CARD_MAX_WIDTH = 420;
117
+ var BUTTON_VERTICAL_PADDING = 14;
118
+ function useAuthStyles(theme) {
119
+ return useMemo(() => {
120
+ const { colors, radii, spacing, typography } = theme;
121
+ return StyleSheet.create({
122
+ screen: {
123
+ flex: 1,
124
+ justifyContent: "center",
125
+ alignItems: "center",
126
+ padding: spacing.md,
127
+ backgroundColor: colors.background
128
+ },
129
+ card: {
130
+ width: FULL_WIDTH,
131
+ maxWidth: CARD_MAX_WIDTH,
132
+ borderRadius: radii.card,
133
+ padding: spacing.xl,
134
+ backgroundColor: colors.surface
135
+ },
136
+ title: {
137
+ fontSize: typography.title,
138
+ fontWeight: "bold",
139
+ textAlign: "center",
140
+ marginBottom: spacing.xs,
141
+ color: colors.text
142
+ },
143
+ subtitle: {
144
+ fontSize: typography.subtitle,
145
+ textAlign: "center",
146
+ marginBottom: spacing.lg,
147
+ color: colors.textSecondary
148
+ },
149
+ fieldGroup: {
150
+ marginBottom: spacing.md
151
+ },
152
+ label: {
153
+ fontSize: typography.label,
154
+ fontWeight: "600",
155
+ marginBottom: spacing.xs,
156
+ color: colors.text
157
+ },
158
+ input: {
159
+ borderWidth: 1,
160
+ borderRadius: radii.input,
161
+ padding: spacing.sm,
162
+ fontSize: typography.body,
163
+ borderColor: colors.border,
164
+ backgroundColor: colors.background,
165
+ color: colors.text
166
+ },
167
+ inputError: {
168
+ borderColor: colors.danger
169
+ },
170
+ primaryButton: {
171
+ borderRadius: radii.input,
172
+ paddingVertical: BUTTON_VERTICAL_PADDING,
173
+ alignItems: "center",
174
+ justifyContent: "center",
175
+ marginTop: spacing.xs,
176
+ backgroundColor: colors.primary
177
+ },
178
+ primaryButtonDisabled: {
179
+ backgroundColor: colors.border
180
+ },
181
+ primaryButtonText: {
182
+ fontSize: typography.body,
183
+ fontWeight: "600",
184
+ color: colors.onPrimary
185
+ },
186
+ linkText: {
187
+ fontSize: typography.caption,
188
+ textDecorationLine: "underline",
189
+ color: colors.primary
190
+ },
191
+ errorText: {
192
+ fontSize: typography.caption,
193
+ marginTop: spacing.xs,
194
+ color: colors.danger
195
+ },
196
+ successText: {
197
+ fontSize: typography.body,
198
+ lineHeight: typography.body * 1.4,
199
+ color: colors.success
200
+ },
201
+ helperText: {
202
+ fontSize: typography.caption,
203
+ color: colors.textSecondary
204
+ }
205
+ });
206
+ }, [theme]);
207
+ }
208
+ var BffAuthStatus = /* @__PURE__ */ ((BffAuthStatus2) => {
209
+ BffAuthStatus2["Loading"] = "loading";
210
+ BffAuthStatus2["Authenticated"] = "authenticated";
211
+ BffAuthStatus2["Unauthenticated"] = "unauthenticated";
212
+ return BffAuthStatus2;
213
+ })(BffAuthStatus || {});
214
+ var INITIAL_LOADING = { user: null, status: "loading" /* Loading */ };
215
+ var INITIAL_IDLE = { user: null, status: "unauthenticated" /* Unauthenticated */ };
216
+ function toError(value) {
217
+ return value instanceof Error ? value : new Error(String(value));
218
+ }
219
+ function useBffAuth(options) {
220
+ const { client, probeOnMount = true } = options;
221
+ const [session, setSession] = useState(
222
+ probeOnMount ? INITIAL_LOADING : INITIAL_IDLE
223
+ );
224
+ const [isSubmitting, setIsSubmitting] = useState(false);
225
+ const [error, setError] = useState(null);
226
+ const mountedRef = useRef(true);
227
+ useEffect(() => {
228
+ mountedRef.current = true;
229
+ return () => {
230
+ mountedRef.current = false;
231
+ };
232
+ }, []);
233
+ const safeSetSession = useCallback((next) => {
234
+ if (mountedRef.current) {
235
+ setSession(next);
236
+ }
237
+ }, []);
238
+ const refresh = useCallback(async () => {
239
+ try {
240
+ const user = await client.getCurrentUser();
241
+ safeSetSession(
242
+ user === null ? INITIAL_IDLE : { user, status: "authenticated" /* Authenticated */ }
243
+ );
244
+ } catch (caught) {
245
+ safeSetSession(INITIAL_IDLE);
246
+ if (mountedRef.current) {
247
+ setError(toError(caught));
248
+ }
249
+ }
250
+ }, [client, safeSetSession]);
251
+ const login = useCallback(
252
+ async (request) => {
253
+ if (mountedRef.current) {
254
+ setIsSubmitting(true);
255
+ setError(null);
256
+ }
257
+ try {
258
+ const user = await client.login(request);
259
+ safeSetSession({ user, status: "authenticated" /* Authenticated */ });
260
+ return user;
261
+ } catch (caught) {
262
+ const err = toError(caught);
263
+ if (mountedRef.current) {
264
+ setError(err);
265
+ }
266
+ throw err;
267
+ } finally {
268
+ if (mountedRef.current) {
269
+ setIsSubmitting(false);
270
+ }
271
+ }
272
+ },
273
+ [client, safeSetSession]
274
+ );
275
+ const logout = useCallback(async () => {
276
+ if (mountedRef.current) {
277
+ setIsSubmitting(true);
278
+ setError(null);
279
+ }
280
+ try {
281
+ await client.logout();
282
+ } catch (caught) {
283
+ if (mountedRef.current) {
284
+ setError(toError(caught));
285
+ }
286
+ } finally {
287
+ safeSetSession(INITIAL_IDLE);
288
+ if (mountedRef.current) {
289
+ setIsSubmitting(false);
290
+ }
291
+ }
292
+ }, [client, safeSetSession]);
293
+ useEffect(() => {
294
+ if (probeOnMount) {
295
+ void refresh();
296
+ }
297
+ }, [probeOnMount, refresh]);
298
+ return useMemo(
299
+ () => ({
300
+ user: session.user,
301
+ status: session.status,
302
+ isSubmitting,
303
+ error,
304
+ login,
305
+ logout,
306
+ refresh
307
+ }),
308
+ [session.user, session.status, isSubmitting, error, login, logout, refresh]
309
+ );
310
+ }
311
+
312
+ // src/theme/AuthTheme.ts
313
+ var defaultAuthTheme = {
314
+ colors: {
315
+ background: "#f5f5f7",
316
+ surface: "#ffffff",
317
+ text: "#1a1a1a",
318
+ textSecondary: "#6b6b6b",
319
+ border: "#d0d0d5",
320
+ primary: "#2563eb",
321
+ onPrimary: "#ffffff",
322
+ danger: "#d33333",
323
+ success: "#1f7a3d"
324
+ },
325
+ radii: {
326
+ input: 8,
327
+ card: 12
328
+ },
329
+ spacing: {
330
+ xs: 8,
331
+ sm: 12,
332
+ md: 20,
333
+ lg: 32,
334
+ xl: 24
335
+ },
336
+ typography: {
337
+ title: 28,
338
+ subtitle: 16,
339
+ label: 14,
340
+ body: 16,
341
+ caption: 12
342
+ }
343
+ };
344
+ var AuthThemeContext = createContext(defaultAuthTheme);
345
+ function AuthThemeProvider({
346
+ theme,
347
+ children
348
+ }) {
349
+ return /* @__PURE__ */ jsx(AuthThemeContext.Provider, { value: theme, children });
350
+ }
351
+ function useAuthTheme(themeProp) {
352
+ const contextTheme = useContext(AuthThemeContext);
353
+ return useMemo(() => themeProp ?? contextTheme, [themeProp, contextTheme]);
354
+ }
355
+ function LoginForm({
356
+ client,
357
+ theme: themeProp,
358
+ labels: labelsProp,
359
+ onSuccess,
360
+ onForgotPassword,
361
+ testIdPrefix
362
+ }) {
363
+ const theme = useAuthTheme(themeProp);
364
+ const styles = useAuthStyles(theme);
365
+ const labels = useMemo(
366
+ () => ({ ...DEFAULT_LOGIN_LABELS, ...labelsProp }),
367
+ [labelsProp]
368
+ );
369
+ const { login, isSubmitting } = useBffAuth({ client, probeOnMount: false });
370
+ const [username, setUsername] = useState("");
371
+ const [password, setPassword] = useState("");
372
+ const [errorText, setErrorText] = useState(null);
373
+ const runLogin = useCallback(async () => {
374
+ setErrorText(null);
375
+ const trimmedUsername = username.trim();
376
+ const hasMissingField = trimmedUsername.length === 0 || password.length === 0;
377
+ if (hasMissingField) {
378
+ setErrorText(labels.missingFields);
379
+ return;
380
+ }
381
+ try {
382
+ const user = await login({ username: trimmedUsername, password });
383
+ onSuccess(user);
384
+ } catch {
385
+ setErrorText(labels.invalidCredentials);
386
+ }
387
+ }, [username, password, login, onSuccess, labels.missingFields, labels.invalidCredentials]);
388
+ const handleSubmit = useCallback(() => {
389
+ void runLogin();
390
+ }, [runLogin]);
391
+ const submitButtonStyle = isSubmitting ? [styles.primaryButton, styles.primaryButtonDisabled] : styles.primaryButton;
392
+ return /* @__PURE__ */ jsx(View, { style: styles.screen, children: /* @__PURE__ */ jsxs(View, { style: styles.card, testID: withTestIdPrefix(AuthTestIds.loginForm, testIdPrefix), children: [
393
+ /* @__PURE__ */ jsx(Text, { style: styles.title, children: labels.title }),
394
+ /* @__PURE__ */ jsx(Text, { style: styles.subtitle, children: labels.subtitle }),
395
+ /* @__PURE__ */ jsxs(View, { style: styles.fieldGroup, children: [
396
+ /* @__PURE__ */ jsx(Text, { style: styles.label, children: labels.usernameLabel }),
397
+ /* @__PURE__ */ jsx(
398
+ TextInput,
399
+ {
400
+ accessibilityHint: labels.usernamePlaceholder,
401
+ accessibilityLabel: labels.usernameLabel,
402
+ autoCapitalize: "none",
403
+ autoCorrect: false,
404
+ editable: !isSubmitting,
405
+ placeholder: labels.usernamePlaceholder,
406
+ placeholderTextColor: theme.colors.textSecondary,
407
+ style: styles.input,
408
+ testID: withTestIdPrefix(AuthTestIds.loginUsernameInput, testIdPrefix),
409
+ value: username,
410
+ onChangeText: setUsername
411
+ }
412
+ )
413
+ ] }),
414
+ /* @__PURE__ */ jsxs(View, { style: styles.fieldGroup, children: [
415
+ /* @__PURE__ */ jsx(Text, { style: styles.label, children: labels.passwordLabel }),
416
+ /* @__PURE__ */ jsx(
417
+ TextInput,
418
+ {
419
+ secureTextEntry: true,
420
+ accessibilityHint: labels.passwordPlaceholder,
421
+ accessibilityLabel: labels.passwordLabel,
422
+ autoCapitalize: "none",
423
+ autoCorrect: false,
424
+ editable: !isSubmitting,
425
+ placeholder: labels.passwordPlaceholder,
426
+ placeholderTextColor: theme.colors.textSecondary,
427
+ style: styles.input,
428
+ testID: withTestIdPrefix(AuthTestIds.loginPasswordInput, testIdPrefix),
429
+ value: password,
430
+ onChangeText: setPassword
431
+ }
432
+ )
433
+ ] }),
434
+ errorText !== null ? /* @__PURE__ */ jsx(
435
+ Text,
436
+ {
437
+ style: styles.errorText,
438
+ testID: withTestIdPrefix(AuthTestIds.loginError, testIdPrefix),
439
+ children: errorText
440
+ }
441
+ ) : null,
442
+ onForgotPassword !== void 0 ? /* @__PURE__ */ jsx(
443
+ TouchableOpacity,
444
+ {
445
+ accessibilityHint: labels.forgotPassword,
446
+ accessibilityLabel: labels.forgotPassword,
447
+ accessibilityRole: "link",
448
+ disabled: isSubmitting,
449
+ style: styles.fieldGroup,
450
+ testID: withTestIdPrefix(AuthTestIds.loginForgotLink, testIdPrefix),
451
+ onPress: onForgotPassword,
452
+ children: /* @__PURE__ */ jsx(Text, { style: styles.linkText, children: labels.forgotPassword })
453
+ }
454
+ ) : null,
455
+ /* @__PURE__ */ jsx(
456
+ TouchableOpacity,
457
+ {
458
+ accessibilityHint: labels.submit,
459
+ accessibilityLabel: isSubmitting ? labels.submitting : labels.submit,
460
+ accessibilityRole: "button",
461
+ disabled: isSubmitting,
462
+ style: submitButtonStyle,
463
+ testID: withTestIdPrefix(AuthTestIds.loginSubmitButton, testIdPrefix),
464
+ onPress: handleSubmit,
465
+ children: isSubmitting ? /* @__PURE__ */ jsx(ActivityIndicator, { color: theme.colors.onPrimary, size: "small" }) : /* @__PURE__ */ jsx(Text, { style: styles.primaryButtonText, children: labels.submit })
466
+ }
467
+ )
468
+ ] }) });
469
+ }
470
+ function useBffForgotPassword(options) {
471
+ const { client, ...rest } = options;
472
+ return useMutation({
473
+ mutationFn: async (request) => {
474
+ await client.forgotPassword(request);
475
+ return void 0;
476
+ },
477
+ ...rest
478
+ });
479
+ }
480
+ function useBffResetPassword(options) {
481
+ const { client, ...rest } = options;
482
+ return useMutation({
483
+ mutationFn: async (request) => {
484
+ await client.resetPassword(request);
485
+ return void 0;
486
+ },
487
+ ...rest
488
+ });
489
+ }
490
+ var EMAIL_REGEX = /^[^@\s]+@[^.\s]+\.[^\s]+$/;
491
+ function isValidEmail(value) {
492
+ return EMAIL_REGEX.test(value);
493
+ }
494
+ function ForgotPasswordForm({
495
+ client,
496
+ theme: themeProp,
497
+ labels: labelsProp,
498
+ resetUrlTemplate,
499
+ onSuccess,
500
+ testIdPrefix
501
+ }) {
502
+ const theme = useAuthTheme(themeProp);
503
+ const styles = useAuthStyles(theme);
504
+ const labels = useMemo(
505
+ () => ({ ...DEFAULT_FORGOT_PASSWORD_LABELS, ...labelsProp }),
506
+ [labelsProp]
507
+ );
508
+ const [email, setEmail] = useState("");
509
+ const [submitted, setSubmitted] = useState(false);
510
+ const [errorText, setErrorText] = useState(null);
511
+ const mutationOptions = useMemo(
512
+ () => ({
513
+ client,
514
+ onSuccess: () => {
515
+ setSubmitted(true);
516
+ onSuccess?.();
517
+ },
518
+ onError: () => {
519
+ setErrorText(labels.networkError);
520
+ }
521
+ }),
522
+ [client, onSuccess, labels.networkError]
523
+ );
524
+ const mutation = useBffForgotPassword(mutationOptions);
525
+ const isPending = mutation.status === "pending";
526
+ const handleSubmit = useCallback(() => {
527
+ setErrorText(null);
528
+ const trimmedEmail = email.trim();
529
+ if (!isValidEmail(trimmedEmail)) {
530
+ setErrorText(labels.invalidEmail);
531
+ return;
532
+ }
533
+ mutation.mutate({ email: trimmedEmail, resetUrlTemplate });
534
+ }, [email, mutation, resetUrlTemplate, labels.invalidEmail]);
535
+ const submitButtonStyle = isPending ? [styles.primaryButton, styles.primaryButtonDisabled] : styles.primaryButton;
536
+ return /* @__PURE__ */ jsx(View, { style: styles.screen, children: /* @__PURE__ */ jsxs(
537
+ View,
538
+ {
539
+ style: styles.card,
540
+ testID: withTestIdPrefix(AuthTestIds.forgotPasswordForm, testIdPrefix),
541
+ children: [
542
+ /* @__PURE__ */ jsx(Text, { style: styles.title, children: labels.title }),
543
+ submitted ? /* @__PURE__ */ jsx(
544
+ Text,
545
+ {
546
+ style: styles.successText,
547
+ testID: withTestIdPrefix(AuthTestIds.forgotPasswordSuccess, testIdPrefix),
548
+ children: labels.successMessage
549
+ }
550
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
551
+ /* @__PURE__ */ jsx(Text, { style: styles.subtitle, children: labels.description }),
552
+ /* @__PURE__ */ jsxs(View, { style: styles.fieldGroup, children: [
553
+ /* @__PURE__ */ jsx(Text, { style: styles.label, children: labels.emailLabel }),
554
+ /* @__PURE__ */ jsx(
555
+ TextInput,
556
+ {
557
+ accessibilityHint: labels.emailPlaceholder,
558
+ accessibilityLabel: labels.emailLabel,
559
+ autoCapitalize: "none",
560
+ autoCorrect: false,
561
+ editable: !isPending,
562
+ keyboardType: "email-address",
563
+ placeholder: labels.emailPlaceholder,
564
+ placeholderTextColor: theme.colors.textSecondary,
565
+ style: styles.input,
566
+ testID: withTestIdPrefix(AuthTestIds.forgotPasswordEmailInput, testIdPrefix),
567
+ value: email,
568
+ onChangeText: setEmail
569
+ }
570
+ )
571
+ ] }),
572
+ errorText !== null ? /* @__PURE__ */ jsx(
573
+ Text,
574
+ {
575
+ style: styles.errorText,
576
+ testID: withTestIdPrefix(AuthTestIds.forgotPasswordError, testIdPrefix),
577
+ children: errorText
578
+ }
579
+ ) : null,
580
+ /* @__PURE__ */ jsx(
581
+ TouchableOpacity,
582
+ {
583
+ accessibilityHint: labels.submit,
584
+ accessibilityLabel: isPending ? labels.submitting : labels.submit,
585
+ accessibilityRole: "button",
586
+ disabled: isPending,
587
+ style: submitButtonStyle,
588
+ testID: withTestIdPrefix(AuthTestIds.forgotPasswordSubmitButton, testIdPrefix),
589
+ onPress: handleSubmit,
590
+ children: isPending ? /* @__PURE__ */ jsx(ActivityIndicator, { color: theme.colors.onPrimary, size: "small" }) : /* @__PURE__ */ jsx(Text, { style: styles.primaryButtonText, children: labels.submit })
591
+ }
592
+ )
593
+ ] })
594
+ ]
595
+ }
596
+ ) });
597
+ }
598
+
599
+ // src/hooks/ResetPasswordError.ts
600
+ var ResetPasswordError = /* @__PURE__ */ ((ResetPasswordError2) => {
601
+ ResetPasswordError2["Empty"] = "empty";
602
+ ResetPasswordError2["WeakPassword"] = "weakPassword";
603
+ ResetPasswordError2["Mismatch"] = "mismatch";
604
+ ResetPasswordError2["TokenInvalid"] = "tokenInvalid";
605
+ ResetPasswordError2["Network"] = "network";
606
+ return ResetPasswordError2;
607
+ })(ResetPasswordError || {});
608
+
609
+ // src/password/PasswordPolicyError.ts
610
+ var PasswordPolicyError = /* @__PURE__ */ ((PasswordPolicyError2) => {
611
+ PasswordPolicyError2["TooShort"] = "tooShort";
612
+ PasswordPolicyError2["TooLong"] = "tooLong";
613
+ PasswordPolicyError2["MissingUppercase"] = "missingUppercase";
614
+ PasswordPolicyError2["MissingLowercase"] = "missingLowercase";
615
+ PasswordPolicyError2["MissingDigit"] = "missingDigit";
616
+ return PasswordPolicyError2;
617
+ })(PasswordPolicyError || {});
618
+
619
+ // src/password/passwordPolicy.ts
620
+ var UPPERCASE_REGEX = /[A-Z]/;
621
+ var LOWERCASE_REGEX = /[a-z]/;
622
+ var DIGIT_REGEX = /\d/;
623
+ var PASSWORD_MIN_LENGTH = 8;
624
+ var PASSWORD_MAX_LENGTH = 128;
625
+ function validatePasswordPolicy(password) {
626
+ const errors = [];
627
+ if (password.length < PASSWORD_MIN_LENGTH) {
628
+ errors.push("tooShort" /* TooShort */);
629
+ }
630
+ if (password.length > PASSWORD_MAX_LENGTH) {
631
+ errors.push("tooLong" /* TooLong */);
632
+ }
633
+ if (!UPPERCASE_REGEX.test(password)) {
634
+ errors.push("missingUppercase" /* MissingUppercase */);
635
+ }
636
+ if (!LOWERCASE_REGEX.test(password)) {
637
+ errors.push("missingLowercase" /* MissingLowercase */);
638
+ }
639
+ if (!DIGIT_REGEX.test(password)) {
640
+ errors.push("missingDigit" /* MissingDigit */);
641
+ }
642
+ return errors;
643
+ }
644
+ function isPasswordValid(password) {
645
+ return validatePasswordPolicy(password).length === 0;
646
+ }
647
+
648
+ // src/hooks/useResetPasswordForm.ts
649
+ var HTTP_BAD_REQUEST = 400;
650
+ function isHttpStatusError(message, status) {
651
+ return message.includes(` status ${String(status)}`);
652
+ }
653
+ function preflight(args) {
654
+ if (args.newPassword.length === 0 || args.confirm.length === 0) {
655
+ return "empty" /* Empty */;
656
+ }
657
+ if (!isPasswordValid(args.newPassword)) {
658
+ return "weakPassword" /* WeakPassword */;
659
+ }
660
+ if (args.newPassword !== args.confirm) {
661
+ return "mismatch" /* Mismatch */;
662
+ }
663
+ if (args.token === "") {
664
+ return "tokenInvalid" /* TokenInvalid */;
665
+ }
666
+ return null;
667
+ }
668
+ function useResetMutation(client, callbacks) {
669
+ const mutationOptions = useMemo(
670
+ () => ({
671
+ client,
672
+ onSuccess: callbacks.onSuccess,
673
+ onError: (err) => {
674
+ if (isHttpStatusError(err.message, HTTP_BAD_REQUEST)) {
675
+ callbacks.setHasInvalidToken(true);
676
+ callbacks.setErrorKey("tokenInvalid" /* TokenInvalid */);
677
+ return;
678
+ }
679
+ callbacks.setErrorKey("network" /* Network */);
680
+ }
681
+ }),
682
+ [client, callbacks]
683
+ );
684
+ return useBffResetPassword(mutationOptions);
685
+ }
686
+ function runSubmit(args) {
687
+ args.setErrorKey(null);
688
+ const failure = preflight({
689
+ newPassword: args.newPassword,
690
+ confirm: args.confirmPassword,
691
+ token: args.token
692
+ });
693
+ if (failure === null) {
694
+ args.mutate({ token: args.token, newPassword: args.newPassword });
695
+ return;
696
+ }
697
+ if (failure === "tokenInvalid" /* TokenInvalid */) {
698
+ args.setHasInvalidToken(true);
699
+ }
700
+ args.setErrorKey(failure);
701
+ }
702
+ function useFormState() {
703
+ const [newPassword, setNewPassword] = useState("");
704
+ const [confirmPassword, setConfirmPassword] = useState("");
705
+ const [errorKey, setErrorKey] = useState(null);
706
+ const [hasInvalidToken, setHasInvalidToken] = useState(false);
707
+ return {
708
+ state: { newPassword, confirmPassword, errorKey, hasInvalidToken },
709
+ setters: { setNewPassword, setConfirmPassword, setErrorKey, setHasInvalidToken }
710
+ };
711
+ }
712
+ function useResetPasswordForm({
713
+ client,
714
+ token,
715
+ onSuccess
716
+ }) {
717
+ const { state, setters } = useFormState();
718
+ const callbacks = useMemo(
719
+ () => ({
720
+ onSuccess,
721
+ setErrorKey: setters.setErrorKey,
722
+ setHasInvalidToken: setters.setHasInvalidToken
723
+ }),
724
+ [onSuccess, setters.setErrorKey, setters.setHasInvalidToken]
725
+ );
726
+ const mutation = useResetMutation(client, callbacks);
727
+ const submit = () => runSubmit({
728
+ newPassword: state.newPassword,
729
+ confirmPassword: state.confirmPassword,
730
+ token,
731
+ setErrorKey: setters.setErrorKey,
732
+ setHasInvalidToken: setters.setHasInvalidToken,
733
+ mutate: mutation.mutate
734
+ });
735
+ return {
736
+ newPassword: state.newPassword,
737
+ confirmPassword: state.confirmPassword,
738
+ setNewPassword: setters.setNewPassword,
739
+ setConfirmPassword: setters.setConfirmPassword,
740
+ isSubmitting: mutation.status === "pending",
741
+ errorKey: state.errorKey,
742
+ hasInvalidToken: state.hasInvalidToken,
743
+ submit
744
+ };
745
+ }
746
+ function errorMessage(errorKey, labels) {
747
+ switch (errorKey) {
748
+ case "empty" /* Empty */:
749
+ return labels.errorEmpty;
750
+ case "weakPassword" /* WeakPassword */:
751
+ return labels.errorWeakPassword;
752
+ case "mismatch" /* Mismatch */:
753
+ return labels.errorMismatch;
754
+ case "tokenInvalid" /* TokenInvalid */:
755
+ return labels.errorTokenInvalid;
756
+ case "network" /* Network */:
757
+ return labels.errorNetwork;
758
+ default:
759
+ return null;
760
+ }
761
+ }
762
+ function ResetPasswordForm({
763
+ client,
764
+ token,
765
+ theme: themeProp,
766
+ labels: labelsProp,
767
+ onSuccess,
768
+ testIdPrefix
769
+ }) {
770
+ const theme = useAuthTheme(themeProp);
771
+ const styles = useAuthStyles(theme);
772
+ const labels = useMemo(
773
+ () => ({ ...DEFAULT_RESET_PASSWORD_LABELS, ...labelsProp }),
774
+ [labelsProp]
775
+ );
776
+ const form = useResetPasswordForm({ client, token, onSuccess });
777
+ const message = errorMessage(form.errorKey, labels);
778
+ const submitButtonStyle = form.isSubmitting ? [styles.primaryButton, styles.primaryButtonDisabled] : styles.primaryButton;
779
+ return /* @__PURE__ */ jsx(View, { style: styles.screen, children: /* @__PURE__ */ jsxs(
780
+ View,
781
+ {
782
+ style: styles.card,
783
+ testID: withTestIdPrefix(AuthTestIds.resetPasswordForm, testIdPrefix),
784
+ children: [
785
+ /* @__PURE__ */ jsx(Text, { style: styles.title, children: labels.title }),
786
+ /* @__PURE__ */ jsx(Text, { style: styles.subtitle, children: labels.description }),
787
+ /* @__PURE__ */ jsxs(View, { style: styles.fieldGroup, children: [
788
+ /* @__PURE__ */ jsx(Text, { style: styles.label, children: labels.newPasswordLabel }),
789
+ /* @__PURE__ */ jsx(
790
+ TextInput,
791
+ {
792
+ secureTextEntry: true,
793
+ accessibilityHint: labels.newPasswordPlaceholder,
794
+ accessibilityLabel: labels.newPasswordLabel,
795
+ autoCapitalize: "none",
796
+ autoCorrect: false,
797
+ editable: !form.isSubmitting,
798
+ placeholder: labels.newPasswordPlaceholder,
799
+ placeholderTextColor: theme.colors.textSecondary,
800
+ style: styles.input,
801
+ testID: withTestIdPrefix(AuthTestIds.resetPasswordNewInput, testIdPrefix),
802
+ value: form.newPassword,
803
+ onChangeText: form.setNewPassword
804
+ }
805
+ )
806
+ ] }),
807
+ /* @__PURE__ */ jsxs(View, { style: styles.fieldGroup, children: [
808
+ /* @__PURE__ */ jsx(Text, { style: styles.label, children: labels.confirmPasswordLabel }),
809
+ /* @__PURE__ */ jsx(
810
+ TextInput,
811
+ {
812
+ secureTextEntry: true,
813
+ accessibilityHint: labels.confirmPasswordPlaceholder,
814
+ accessibilityLabel: labels.confirmPasswordLabel,
815
+ autoCapitalize: "none",
816
+ autoCorrect: false,
817
+ editable: !form.isSubmitting,
818
+ placeholder: labels.confirmPasswordPlaceholder,
819
+ placeholderTextColor: theme.colors.textSecondary,
820
+ style: styles.input,
821
+ testID: withTestIdPrefix(AuthTestIds.resetPasswordConfirmInput, testIdPrefix),
822
+ value: form.confirmPassword,
823
+ onChangeText: form.setConfirmPassword
824
+ }
825
+ )
826
+ ] }),
827
+ message !== null ? /* @__PURE__ */ jsx(
828
+ Text,
829
+ {
830
+ style: styles.errorText,
831
+ testID: withTestIdPrefix(AuthTestIds.resetPasswordError, testIdPrefix),
832
+ children: message
833
+ }
834
+ ) : null,
835
+ /* @__PURE__ */ jsx(
836
+ TouchableOpacity,
837
+ {
838
+ accessibilityHint: labels.submit,
839
+ accessibilityLabel: form.isSubmitting ? labels.submitting : labels.submit,
840
+ accessibilityRole: "button",
841
+ disabled: form.isSubmitting,
842
+ style: submitButtonStyle,
843
+ testID: withTestIdPrefix(AuthTestIds.resetPasswordSubmitButton, testIdPrefix),
844
+ onPress: form.submit,
845
+ children: form.isSubmitting ? /* @__PURE__ */ jsx(ActivityIndicator, { color: theme.colors.onPrimary, size: "small" }) : /* @__PURE__ */ jsx(Text, { style: styles.primaryButtonText, children: labels.submit })
846
+ }
847
+ )
848
+ ]
849
+ }
850
+ ) });
851
+ }
852
+ function OtpRequestStep({
853
+ styles,
854
+ theme,
855
+ labels,
856
+ testIdPrefix,
857
+ isSubmitting,
858
+ errorText,
859
+ onSubmit
860
+ }) {
861
+ const [email, setEmail] = useState("");
862
+ const buttonStyle = isSubmitting ? [styles.primaryButton, styles.primaryButtonDisabled] : styles.primaryButton;
863
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
864
+ /* @__PURE__ */ jsx(Text, { style: styles.title, children: labels.requestTitle }),
865
+ /* @__PURE__ */ jsx(Text, { style: styles.subtitle, children: labels.requestDescription }),
866
+ /* @__PURE__ */ jsxs(View, { style: styles.fieldGroup, children: [
867
+ /* @__PURE__ */ jsx(Text, { style: styles.label, children: labels.emailLabel }),
868
+ /* @__PURE__ */ jsx(
869
+ TextInput,
870
+ {
871
+ accessibilityHint: labels.emailPlaceholder,
872
+ accessibilityLabel: labels.emailLabel,
873
+ autoCapitalize: "none",
874
+ autoCorrect: false,
875
+ editable: !isSubmitting,
876
+ keyboardType: "email-address",
877
+ placeholder: labels.emailPlaceholder,
878
+ placeholderTextColor: theme.colors.textSecondary,
879
+ style: styles.input,
880
+ testID: withTestIdPrefix(AuthTestIds.otpEmailInput, testIdPrefix),
881
+ value: email,
882
+ onChangeText: setEmail
883
+ }
884
+ )
885
+ ] }),
886
+ errorText !== null ? /* @__PURE__ */ jsx(Text, { style: styles.errorText, testID: withTestIdPrefix(AuthTestIds.otpError, testIdPrefix), children: errorText }) : null,
887
+ /* @__PURE__ */ jsx(
888
+ TouchableOpacity,
889
+ {
890
+ accessibilityHint: labels.requestSubmit,
891
+ accessibilityLabel: isSubmitting ? labels.requesting : labels.requestSubmit,
892
+ accessibilityRole: "button",
893
+ disabled: isSubmitting,
894
+ style: buttonStyle,
895
+ testID: withTestIdPrefix(AuthTestIds.otpRequestButton, testIdPrefix),
896
+ onPress: () => onSubmit(email.trim()),
897
+ children: isSubmitting ? /* @__PURE__ */ jsx(ActivityIndicator, { color: theme.colors.onPrimary, size: "small" }) : /* @__PURE__ */ jsx(Text, { style: styles.primaryButtonText, children: labels.requestSubmit })
898
+ }
899
+ )
900
+ ] });
901
+ }
902
+ function OtpVerifyStep({
903
+ styles,
904
+ theme,
905
+ labels,
906
+ testIdPrefix,
907
+ identifier,
908
+ isSubmitting,
909
+ errorText,
910
+ onSubmit,
911
+ onResend,
912
+ onChangeEmail
913
+ }) {
914
+ const [code, setCode] = useState("");
915
+ const buttonStyle = isSubmitting ? [styles.primaryButton, styles.primaryButtonDisabled] : styles.primaryButton;
916
+ const description = labels.verifyDescription.replace("{identifier}", identifier);
917
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
918
+ /* @__PURE__ */ jsx(Text, { style: styles.title, children: labels.verifyTitle }),
919
+ /* @__PURE__ */ jsx(Text, { style: styles.subtitle, children: description }),
920
+ /* @__PURE__ */ jsxs(View, { style: styles.fieldGroup, children: [
921
+ /* @__PURE__ */ jsx(Text, { style: styles.label, children: labels.codeLabel }),
922
+ /* @__PURE__ */ jsx(
923
+ TextInput,
924
+ {
925
+ accessibilityHint: labels.codePlaceholder,
926
+ accessibilityLabel: labels.codeLabel,
927
+ autoCapitalize: "none",
928
+ autoCorrect: false,
929
+ editable: !isSubmitting,
930
+ keyboardType: "number-pad",
931
+ placeholder: labels.codePlaceholder,
932
+ placeholderTextColor: theme.colors.textSecondary,
933
+ style: styles.input,
934
+ testID: withTestIdPrefix(AuthTestIds.otpCodeInput, testIdPrefix),
935
+ value: code,
936
+ onChangeText: setCode
937
+ }
938
+ )
939
+ ] }),
940
+ errorText !== null ? /* @__PURE__ */ jsx(Text, { style: styles.errorText, testID: withTestIdPrefix(AuthTestIds.otpError, testIdPrefix), children: errorText }) : null,
941
+ /* @__PURE__ */ jsx(
942
+ TouchableOpacity,
943
+ {
944
+ accessibilityHint: labels.verifySubmit,
945
+ accessibilityLabel: isSubmitting ? labels.verifying : labels.verifySubmit,
946
+ accessibilityRole: "button",
947
+ disabled: isSubmitting,
948
+ style: buttonStyle,
949
+ testID: withTestIdPrefix(AuthTestIds.otpVerifyButton, testIdPrefix),
950
+ onPress: () => onSubmit(code.trim()),
951
+ children: isSubmitting ? /* @__PURE__ */ jsx(ActivityIndicator, { color: theme.colors.onPrimary, size: "small" }) : /* @__PURE__ */ jsx(Text, { style: styles.primaryButtonText, children: labels.verifySubmit })
952
+ }
953
+ ),
954
+ /* @__PURE__ */ jsx(
955
+ TouchableOpacity,
956
+ {
957
+ accessibilityHint: labels.resend,
958
+ accessibilityLabel: isSubmitting ? labels.resending : labels.resend,
959
+ accessibilityRole: "link",
960
+ disabled: isSubmitting,
961
+ style: styles.fieldGroup,
962
+ testID: withTestIdPrefix(AuthTestIds.otpResendButton, testIdPrefix),
963
+ onPress: onResend,
964
+ children: /* @__PURE__ */ jsx(Text, { style: styles.linkText, children: isSubmitting ? labels.resending : labels.resend })
965
+ }
966
+ ),
967
+ /* @__PURE__ */ jsx(
968
+ TouchableOpacity,
969
+ {
970
+ accessibilityHint: labels.changeEmail,
971
+ accessibilityLabel: labels.changeEmail,
972
+ accessibilityRole: "link",
973
+ disabled: isSubmitting,
974
+ testID: withTestIdPrefix(AuthTestIds.otpChangeEmailButton, testIdPrefix),
975
+ onPress: onChangeEmail,
976
+ children: /* @__PURE__ */ jsx(Text, { style: styles.linkText, children: labels.changeEmail })
977
+ }
978
+ )
979
+ ] });
980
+ }
981
+
982
+ // src/hooks/OtpLoginStep.ts
983
+ var OtpLoginStep = /* @__PURE__ */ ((OtpLoginStep2) => {
984
+ OtpLoginStep2["RequestCode"] = "requestCode";
985
+ OtpLoginStep2["EnterCode"] = "enterCode";
986
+ return OtpLoginStep2;
987
+ })(OtpLoginStep || {});
988
+ function toError2(value) {
989
+ return value instanceof Error ? value : new Error(String(value));
990
+ }
991
+ var INITIAL_FLOW = {
992
+ step: "requestCode" /* RequestCode */,
993
+ identifier: "",
994
+ lastRequest: null
995
+ };
996
+ function useOtpLogin(options) {
997
+ const { client } = options;
998
+ const [flow, setFlow] = useState(INITIAL_FLOW);
999
+ const [isSubmitting, setIsSubmitting] = useState(false);
1000
+ const [error, setError] = useState(null);
1001
+ const mountedRef = useRef(true);
1002
+ useEffect(() => {
1003
+ mountedRef.current = true;
1004
+ return () => {
1005
+ mountedRef.current = false;
1006
+ };
1007
+ }, []);
1008
+ const identifierRef = useRef("");
1009
+ const beginAttempt = useCallback(() => {
1010
+ if (mountedRef.current) {
1011
+ setIsSubmitting(true);
1012
+ setError(null);
1013
+ }
1014
+ }, []);
1015
+ const endAttempt = useCallback(() => {
1016
+ if (mountedRef.current) {
1017
+ setIsSubmitting(false);
1018
+ }
1019
+ }, []);
1020
+ const sendCode = useCallback(
1021
+ async (identifier) => {
1022
+ beginAttempt();
1023
+ try {
1024
+ const result = await client.requestOtp({ identifier });
1025
+ identifierRef.current = identifier;
1026
+ if (mountedRef.current) {
1027
+ setFlow({ step: "enterCode" /* EnterCode */, identifier, lastRequest: result });
1028
+ }
1029
+ return result;
1030
+ } catch (caught) {
1031
+ const err = toError2(caught);
1032
+ if (mountedRef.current) {
1033
+ setError(err);
1034
+ }
1035
+ throw err;
1036
+ } finally {
1037
+ endAttempt();
1038
+ }
1039
+ },
1040
+ [client, beginAttempt, endAttempt]
1041
+ );
1042
+ const requestCode = useCallback(
1043
+ (identifier) => sendCode(identifier),
1044
+ [sendCode]
1045
+ );
1046
+ const resend = useCallback(
1047
+ () => sendCode(identifierRef.current),
1048
+ [sendCode]
1049
+ );
1050
+ const verifyCode = useCallback(
1051
+ async (otp) => {
1052
+ beginAttempt();
1053
+ try {
1054
+ return await client.verifyOtp({ username: identifierRef.current, otp });
1055
+ } catch (caught) {
1056
+ const err = toError2(caught);
1057
+ if (mountedRef.current) {
1058
+ setError(err);
1059
+ }
1060
+ throw err;
1061
+ } finally {
1062
+ endAttempt();
1063
+ }
1064
+ },
1065
+ [client, beginAttempt, endAttempt]
1066
+ );
1067
+ const reset = useCallback(() => {
1068
+ identifierRef.current = "";
1069
+ if (mountedRef.current) {
1070
+ setFlow(INITIAL_FLOW);
1071
+ setError(null);
1072
+ }
1073
+ }, []);
1074
+ return useMemo(
1075
+ () => ({
1076
+ step: flow.step,
1077
+ identifier: flow.identifier,
1078
+ lastRequest: flow.lastRequest,
1079
+ isSubmitting,
1080
+ error,
1081
+ requestCode,
1082
+ verifyCode,
1083
+ resend,
1084
+ reset
1085
+ }),
1086
+ [
1087
+ flow.step,
1088
+ flow.identifier,
1089
+ flow.lastRequest,
1090
+ isSubmitting,
1091
+ error,
1092
+ requestCode,
1093
+ verifyCode,
1094
+ resend,
1095
+ reset
1096
+ ]
1097
+ );
1098
+ }
1099
+ var EMAIL_REGEX2 = /^[^@\s]+@[^.\s]+\.[^\s]+$/;
1100
+ function isValidEmail2(value) {
1101
+ return EMAIL_REGEX2.test(value);
1102
+ }
1103
+ function transportErrorFor(step, error, labels) {
1104
+ if (error === null) {
1105
+ return null;
1106
+ }
1107
+ return step === "requestCode" /* RequestCode */ ? labels.invalidEmail : labels.invalidCode;
1108
+ }
1109
+ function useRequestHandler(otp, labels, setLocalError) {
1110
+ return useCallback(
1111
+ (email) => {
1112
+ setLocalError(null);
1113
+ if (!isValidEmail2(email)) {
1114
+ setLocalError(labels.invalidEmail);
1115
+ return;
1116
+ }
1117
+ void otp.requestCode(email).catch(() => void 0);
1118
+ },
1119
+ [otp, labels.invalidEmail, setLocalError]
1120
+ );
1121
+ }
1122
+ function useVerifyHandler(otp, labels, setLocalError, onSuccess) {
1123
+ return useCallback(
1124
+ (code) => {
1125
+ setLocalError(null);
1126
+ if (code.length === 0) {
1127
+ setLocalError(labels.missingCode);
1128
+ return;
1129
+ }
1130
+ otp.verifyCode(code).then((user) => {
1131
+ onSuccess(user);
1132
+ }).catch(() => void 0);
1133
+ },
1134
+ [otp, labels.missingCode, setLocalError, onSuccess]
1135
+ );
1136
+ }
1137
+ function OtpForm({
1138
+ client,
1139
+ theme: themeProp,
1140
+ labels: labelsProp,
1141
+ onSuccess,
1142
+ testIdPrefix
1143
+ }) {
1144
+ const theme = useAuthTheme(themeProp);
1145
+ const styles = useAuthStyles(theme);
1146
+ const labels = useMemo(
1147
+ () => ({ ...DEFAULT_OTP_LABELS, ...labelsProp }),
1148
+ [labelsProp]
1149
+ );
1150
+ const otp = useOtpLogin({ client });
1151
+ const [localError, setLocalError] = useState(null);
1152
+ const handleRequest = useRequestHandler(otp, labels, setLocalError);
1153
+ const handleVerify = useVerifyHandler(otp, labels, setLocalError, onSuccess);
1154
+ const handleResend = useCallback(() => {
1155
+ setLocalError(null);
1156
+ void otp.resend().catch(() => void 0);
1157
+ }, [otp]);
1158
+ const handleChangeEmail = useCallback(() => {
1159
+ setLocalError(null);
1160
+ otp.reset();
1161
+ }, [otp]);
1162
+ const errorText = localError ?? transportErrorFor(otp.step, otp.error, labels);
1163
+ return /* @__PURE__ */ jsx(View, { style: styles.screen, children: /* @__PURE__ */ jsx(View, { style: styles.card, testID: withTestIdPrefix(AuthTestIds.otpForm, testIdPrefix), children: otp.step === "requestCode" /* RequestCode */ ? /* @__PURE__ */ jsx(
1164
+ OtpRequestStep,
1165
+ {
1166
+ errorText,
1167
+ isSubmitting: otp.isSubmitting,
1168
+ labels,
1169
+ styles,
1170
+ testIdPrefix,
1171
+ theme,
1172
+ onSubmit: handleRequest
1173
+ }
1174
+ ) : /* @__PURE__ */ jsx(
1175
+ OtpVerifyStep,
1176
+ {
1177
+ errorText,
1178
+ identifier: otp.identifier,
1179
+ isSubmitting: otp.isSubmitting,
1180
+ labels,
1181
+ styles,
1182
+ testIdPrefix,
1183
+ theme,
1184
+ onChangeEmail: handleChangeEmail,
1185
+ onResend: handleResend,
1186
+ onSubmit: handleVerify
1187
+ }
1188
+ ) }) });
1189
+ }
1190
+ function toError3(value) {
1191
+ return value instanceof Error ? value : new Error(String(value));
1192
+ }
1193
+ function usePinLogin(options) {
1194
+ const { client, eventExternalId } = options;
1195
+ const [isSubmitting, setIsSubmitting] = useState(false);
1196
+ const [error, setError] = useState(null);
1197
+ const mountedRef = useRef(true);
1198
+ useEffect(() => {
1199
+ mountedRef.current = true;
1200
+ return () => {
1201
+ mountedRef.current = false;
1202
+ };
1203
+ }, []);
1204
+ const eventIdRef = useRef(eventExternalId);
1205
+ useEffect(() => {
1206
+ eventIdRef.current = eventExternalId;
1207
+ }, [eventExternalId]);
1208
+ const submit = useCallback(
1209
+ async (pin) => {
1210
+ if (mountedRef.current) {
1211
+ setIsSubmitting(true);
1212
+ setError(null);
1213
+ }
1214
+ try {
1215
+ return await client.pinLogin({ pin, eventExternalId: eventIdRef.current });
1216
+ } catch (caught) {
1217
+ const err = toError3(caught);
1218
+ if (mountedRef.current) {
1219
+ setError(err);
1220
+ }
1221
+ throw err;
1222
+ } finally {
1223
+ if (mountedRef.current) {
1224
+ setIsSubmitting(false);
1225
+ }
1226
+ }
1227
+ },
1228
+ [client]
1229
+ );
1230
+ const reset = useCallback(() => {
1231
+ if (mountedRef.current) {
1232
+ setError(null);
1233
+ }
1234
+ }, []);
1235
+ return useMemo(
1236
+ () => ({ isSubmitting, error, submit, reset }),
1237
+ [isSubmitting, error, submit, reset]
1238
+ );
1239
+ }
1240
+ function transportErrorFor2(error, labels) {
1241
+ if (error === null) {
1242
+ return null;
1243
+ }
1244
+ return labels.invalidPin;
1245
+ }
1246
+ function useSubmitHandler(pin, labels, setLocalError, onSuccess) {
1247
+ return useCallback(
1248
+ (value) => {
1249
+ setLocalError(null);
1250
+ if (value.length === 0) {
1251
+ setLocalError(labels.missingPin);
1252
+ return;
1253
+ }
1254
+ pin.submit(value).then((user) => {
1255
+ onSuccess(user);
1256
+ }).catch(() => void 0);
1257
+ },
1258
+ [pin, labels.missingPin, setLocalError, onSuccess]
1259
+ );
1260
+ }
1261
+ function PinForm({
1262
+ client,
1263
+ eventExternalId,
1264
+ theme: themeProp,
1265
+ labels: labelsProp,
1266
+ onSuccess,
1267
+ testIdPrefix
1268
+ }) {
1269
+ const theme = useAuthTheme(themeProp);
1270
+ const styles = useAuthStyles(theme);
1271
+ const labels = useMemo(
1272
+ () => ({ ...DEFAULT_PIN_LABELS, ...labelsProp }),
1273
+ [labelsProp]
1274
+ );
1275
+ const pin = usePinLogin({ client, eventExternalId });
1276
+ const [localError, setLocalError] = useState(null);
1277
+ const [pinValue, setPinValue] = useState("");
1278
+ const handleSubmit = useSubmitHandler(pin, labels, setLocalError, onSuccess);
1279
+ const errorText = localError ?? transportErrorFor2(pin.error, labels);
1280
+ const buttonStyle = pin.isSubmitting ? [styles.primaryButton, styles.primaryButtonDisabled] : styles.primaryButton;
1281
+ return /* @__PURE__ */ jsx(View, { style: styles.screen, children: /* @__PURE__ */ jsxs(View, { style: styles.card, testID: withTestIdPrefix(AuthTestIds.pinForm, testIdPrefix), children: [
1282
+ /* @__PURE__ */ jsx(Text, { style: styles.title, children: labels.title }),
1283
+ /* @__PURE__ */ jsx(Text, { style: styles.subtitle, children: labels.description }),
1284
+ /* @__PURE__ */ jsxs(View, { style: styles.fieldGroup, children: [
1285
+ /* @__PURE__ */ jsx(Text, { style: styles.label, children: labels.pinLabel }),
1286
+ /* @__PURE__ */ jsx(
1287
+ TextInput,
1288
+ {
1289
+ accessibilityHint: labels.pinPlaceholder,
1290
+ accessibilityLabel: labels.pinLabel,
1291
+ autoCapitalize: "none",
1292
+ autoCorrect: false,
1293
+ editable: !pin.isSubmitting,
1294
+ keyboardType: "number-pad",
1295
+ placeholder: labels.pinPlaceholder,
1296
+ placeholderTextColor: theme.colors.textSecondary,
1297
+ secureTextEntry: true,
1298
+ style: styles.input,
1299
+ testID: withTestIdPrefix(AuthTestIds.pinInput, testIdPrefix),
1300
+ value: pinValue,
1301
+ onChangeText: setPinValue
1302
+ }
1303
+ )
1304
+ ] }),
1305
+ errorText !== null ? /* @__PURE__ */ jsx(
1306
+ Text,
1307
+ {
1308
+ style: styles.errorText,
1309
+ testID: withTestIdPrefix(AuthTestIds.pinError, testIdPrefix),
1310
+ children: errorText
1311
+ }
1312
+ ) : null,
1313
+ /* @__PURE__ */ jsx(
1314
+ TouchableOpacity,
1315
+ {
1316
+ accessibilityHint: labels.submit,
1317
+ accessibilityLabel: pin.isSubmitting ? labels.submitting : labels.submit,
1318
+ accessibilityRole: "button",
1319
+ disabled: pin.isSubmitting,
1320
+ style: buttonStyle,
1321
+ testID: withTestIdPrefix(AuthTestIds.pinSubmitButton, testIdPrefix),
1322
+ onPress: () => handleSubmit(pinValue.trim()),
1323
+ children: pin.isSubmitting ? /* @__PURE__ */ jsx(ActivityIndicator, { color: theme.colors.onPrimary, size: "small" }) : /* @__PURE__ */ jsx(Text, { style: styles.primaryButtonText, children: labels.submit })
1324
+ }
1325
+ )
1326
+ ] }) });
1327
+ }
1328
+ async function lazyFetchHttpClient(request) {
1329
+ const fetchImpl = typeof fetch === "function" ? fetch.bind(globalThis) : void 0;
1330
+ if (fetchImpl === void 0) {
1331
+ throw new Error("createBffAuthClient: fetch is not available in this environment");
1332
+ }
1333
+ return createFetchHttpClient(fetchImpl)(request);
1334
+ }
1335
+ function createBffAuthClient(options = {}) {
1336
+ return new BffAuthClient({
1337
+ http: options.http ?? lazyFetchHttpClient,
1338
+ baseUrl: options.baseUrl
1339
+ });
1340
+ }
1341
+
1342
+ // src/router/resolvePostLoginRoute.ts
1343
+ function isRecord(value) {
1344
+ return typeof value === "object" && value !== null;
1345
+ }
1346
+ function readRealmAccessRoles(realmAccess) {
1347
+ if (!isRecord(realmAccess)) {
1348
+ return [];
1349
+ }
1350
+ const roles = realmAccess.roles;
1351
+ if (!Array.isArray(roles)) {
1352
+ return [];
1353
+ }
1354
+ return roles.filter((role) => typeof role === "string");
1355
+ }
1356
+ function collectUserRoles(user) {
1357
+ const flat = Array.isArray(user.roles) ? user.roles.filter((role) => typeof role === "string") : [];
1358
+ const realm = readRealmAccessRoles(user.realm_access);
1359
+ return Array.from(/* @__PURE__ */ new Set([...flat, ...realm]));
1360
+ }
1361
+ function resolvePostLoginRoute(user, table) {
1362
+ const userRoles = new Set(collectUserRoles(user));
1363
+ for (const entry of table.routes) {
1364
+ if (userRoles.has(entry.role)) {
1365
+ return entry.route;
1366
+ }
1367
+ }
1368
+ return table.fallback ?? null;
1369
+ }
1370
+
1371
+ export { AuthTestIds, AuthThemeProvider, BffAuthStatus, DEFAULT_FORGOT_PASSWORD_LABELS, DEFAULT_LOGIN_LABELS, DEFAULT_OTP_LABELS, DEFAULT_PIN_LABELS, DEFAULT_RESET_PASSWORD_LABELS, ForgotPasswordForm, LoginForm, OtpForm, OtpLoginStep, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH, PasswordPolicyError, PinForm, ResetPasswordError, ResetPasswordForm, collectUserRoles, createBffAuthClient, defaultAuthTheme, isPasswordValid, resolvePostLoginRoute, useAuthTheme, useBffAuth, useBffForgotPassword, useBffResetPassword, useOtpLogin, usePinLogin, useResetPasswordForm, validatePasswordPolicy, withTestIdPrefix };
1372
+ //# sourceMappingURL=index.mjs.map
1373
+ //# sourceMappingURL=index.mjs.map