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