@dloizides/auth-web 1.22.1 → 1.23.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 CHANGED
@@ -306,7 +306,8 @@ var DEFAULT_RESET_PASSWORD_LABELS = {
306
306
  errorWeakPassword: "Use at least 8 characters with an uppercase letter, a lowercase letter and a digit.",
307
307
  errorMismatch: "The passwords do not match.",
308
308
  errorTokenInvalid: "This reset link is invalid or has expired. Request a new one.",
309
- errorNetwork: "Something went wrong. Please try again."
309
+ errorNetwork: "Something went wrong. Please try again.",
310
+ errorRateLimited: "Too many attempts. Wait a minute and try again."
310
311
  };
311
312
 
312
313
  // src/components/labels/accountSettingsLabels.ts
@@ -343,6 +344,8 @@ var DEFAULT_REGISTER_LABELS = {
343
344
  submitHint: "Creates your account",
344
345
  submitting: "Creating account...",
345
346
  fullName: field("Your name", "Enter your full name"),
347
+ checkEmailTitle: "Check your email",
348
+ checkEmailBody: "If this address can be used, a link to finish signing up is on its way.",
346
349
  fields: {
347
350
  firstName: field("First name", "Enter your first name"),
348
351
  lastName: field("Last name", "Enter your last name"),
@@ -355,6 +358,19 @@ var DEFAULT_REGISTER_LABELS = {
355
358
  };
356
359
 
357
360
  // src/components/labels/verifyEmailLabels.ts
361
+ var DEFAULT_VERIFY_NEW_PASSWORD_LABELS = {
362
+ ...DEFAULT_RESET_PASSWORD_LABELS,
363
+ title: "Set a new password",
364
+ description: "To finish signing in, choose a new password for your account.",
365
+ submit: "Save and sign in",
366
+ submitting: "Saving..."
367
+ };
368
+ var DEFAULT_VERIFY_SIGN_IN_LABELS = {
369
+ title: "Your email is verified",
370
+ body: "We could not sign you in automatically. Please sign in with your password.",
371
+ action: "Sign in",
372
+ actionHint: "Go to the sign in page"
373
+ };
358
374
  var DEFAULT_VERIFY_EMAIL_LABELS = {
359
375
  loadingTitle: "Verifying your email...",
360
376
  loadingHint: "Please wait while we verify your address.",
@@ -376,7 +392,10 @@ var DEFAULT_VERIFY_EMAIL_LABELS = {
376
392
  resendMissingEmail: "Enter your email address.",
377
393
  resendSubmit: "Send link",
378
394
  resendSubmitHint: "Sends a new verification email",
379
- resendConfirmation: "If that address has an account, a new verification link is on its way."
395
+ resendConfirmation: "If that address has an account, a new verification link is on its way.",
396
+ errorRateLimited: "Too many attempts. Wait a minute and try again.",
397
+ newPassword: DEFAULT_VERIFY_NEW_PASSWORD_LABELS,
398
+ signIn: DEFAULT_VERIFY_SIGN_IN_LABELS
380
399
  };
381
400
 
382
401
  // src/components/testIds.ts
@@ -475,6 +494,8 @@ var AuthTestIds = {
475
494
  verifyEmailSuccess: "auth-verify-email-success",
476
495
  verifyEmailContinue: "auth-verify-email-continue",
477
496
  verifyEmailError: "auth-verify-email-error",
497
+ verifyEmailSignIn: "auth-verify-email-sign-in",
498
+ verifyEmailSignInContinue: "auth-verify-email-sign-in-continue",
478
499
  verifyEmailResendCta: "auth-verify-email-resend-cta",
479
500
  verifyEmailResendForm: "auth-verify-email-resend-form",
480
501
  verifyEmailResendEmail: "auth-verify-email-resend-email",
@@ -1275,8 +1296,107 @@ var ResetPasswordError = /* @__PURE__ */ ((ResetPasswordError2) => {
1275
1296
  ResetPasswordError2["Mismatch"] = "mismatch";
1276
1297
  ResetPasswordError2["TokenInvalid"] = "tokenInvalid";
1277
1298
  ResetPasswordError2["Network"] = "network";
1299
+ ResetPasswordError2["RateLimited"] = "rateLimited";
1278
1300
  return ResetPasswordError2;
1279
1301
  })(ResetPasswordError || {});
1302
+ function errorMessage(errorKey, labels) {
1303
+ switch (errorKey) {
1304
+ case "empty" /* Empty */:
1305
+ return labels.errorEmpty;
1306
+ case "weakPassword" /* WeakPassword */:
1307
+ return labels.errorWeakPassword;
1308
+ case "mismatch" /* Mismatch */:
1309
+ return labels.errorMismatch;
1310
+ case "tokenInvalid" /* TokenInvalid */:
1311
+ return labels.errorTokenInvalid;
1312
+ case "network" /* Network */:
1313
+ return labels.errorNetwork;
1314
+ case "rateLimited" /* RateLimited */:
1315
+ return labels.errorRateLimited ?? labels.errorNetwork;
1316
+ default:
1317
+ return null;
1318
+ }
1319
+ }
1320
+ function ResetPasswordFields({
1321
+ form,
1322
+ labels,
1323
+ styles: styles3,
1324
+ theme,
1325
+ testIdPrefix,
1326
+ newPasswordRef,
1327
+ onSubmitEditing
1328
+ }) {
1329
+ const message = errorMessage(form.errorKey, labels);
1330
+ const submitButtonStyle = form.isSubmitting ? [styles3.primaryButton, styles3.primaryButtonDisabled] : styles3.primaryButton;
1331
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1332
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.title, children: labels.title }),
1333
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.subtitle, children: labels.description }),
1334
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.fieldGroup, children: [
1335
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.label, children: labels.newPasswordLabel }),
1336
+ /* @__PURE__ */ jsxRuntime.jsx(
1337
+ reactNative.TextInput,
1338
+ {
1339
+ ref: newPasswordRef,
1340
+ secureTextEntry: true,
1341
+ accessibilityHint: labels.newPasswordPlaceholder,
1342
+ accessibilityLabel: labels.newPasswordLabel,
1343
+ autoCapitalize: "none",
1344
+ autoCorrect: false,
1345
+ editable: !form.isSubmitting,
1346
+ placeholder: labels.newPasswordPlaceholder,
1347
+ placeholderTextColor: theme.colors.textSecondary,
1348
+ style: styles3.input,
1349
+ testID: withTestIdPrefix(AuthTestIds.resetPasswordNewInput, testIdPrefix),
1350
+ value: form.newPassword,
1351
+ onChangeText: form.setNewPassword,
1352
+ onSubmitEditing
1353
+ }
1354
+ )
1355
+ ] }),
1356
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.fieldGroup, children: [
1357
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.label, children: labels.confirmPasswordLabel }),
1358
+ /* @__PURE__ */ jsxRuntime.jsx(
1359
+ reactNative.TextInput,
1360
+ {
1361
+ secureTextEntry: true,
1362
+ accessibilityHint: labels.confirmPasswordPlaceholder,
1363
+ accessibilityLabel: labels.confirmPasswordLabel,
1364
+ autoCapitalize: "none",
1365
+ autoCorrect: false,
1366
+ editable: !form.isSubmitting,
1367
+ placeholder: labels.confirmPasswordPlaceholder,
1368
+ placeholderTextColor: theme.colors.textSecondary,
1369
+ style: styles3.input,
1370
+ testID: withTestIdPrefix(AuthTestIds.resetPasswordConfirmInput, testIdPrefix),
1371
+ value: form.confirmPassword,
1372
+ onChangeText: form.setConfirmPassword,
1373
+ onSubmitEditing
1374
+ }
1375
+ )
1376
+ ] }),
1377
+ message !== null ? /* @__PURE__ */ jsxRuntime.jsx(
1378
+ reactNative.Text,
1379
+ {
1380
+ style: styles3.errorText,
1381
+ testID: withTestIdPrefix(AuthTestIds.resetPasswordError, testIdPrefix),
1382
+ children: message
1383
+ }
1384
+ ) : null,
1385
+ /* @__PURE__ */ jsxRuntime.jsx(
1386
+ reactNative.TouchableOpacity,
1387
+ {
1388
+ accessibilityHint: labels.submit,
1389
+ accessibilityLabel: form.isSubmitting ? labels.submitting : labels.submit,
1390
+ accessibilityRole: "button",
1391
+ disabled: form.isSubmitting,
1392
+ style: submitButtonStyle,
1393
+ testID: withTestIdPrefix(AuthTestIds.resetPasswordSubmitButton, testIdPrefix),
1394
+ onPress: form.submit,
1395
+ children: form.isSubmitting ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.onPrimary, size: "small" }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.primaryButtonText, children: labels.submit })
1396
+ }
1397
+ )
1398
+ ] });
1399
+ }
1280
1400
 
1281
1401
  // src/password/PasswordPolicyError.ts
1282
1402
  var PasswordPolicyError = /* @__PURE__ */ ((PasswordPolicyError2) => {
@@ -1415,22 +1535,6 @@ function useResetPasswordForm({
1415
1535
  submit
1416
1536
  };
1417
1537
  }
1418
- function errorMessage(errorKey, labels) {
1419
- switch (errorKey) {
1420
- case "empty" /* Empty */:
1421
- return labels.errorEmpty;
1422
- case "weakPassword" /* WeakPassword */:
1423
- return labels.errorWeakPassword;
1424
- case "mismatch" /* Mismatch */:
1425
- return labels.errorMismatch;
1426
- case "tokenInvalid" /* TokenInvalid */:
1427
- return labels.errorTokenInvalid;
1428
- case "network" /* Network */:
1429
- return labels.errorNetwork;
1430
- default:
1431
- return null;
1432
- }
1433
- }
1434
1538
  function ResetPasswordForm({
1435
1539
  client,
1436
1540
  token,
@@ -1446,78 +1550,21 @@ function ResetPasswordForm({
1446
1550
  [labelsProp]
1447
1551
  );
1448
1552
  const form = useResetPasswordForm({ client, token, onSuccess });
1449
- const message = errorMessage(form.errorKey, labels);
1450
- const submitButtonStyle = form.isSubmitting ? [styles3.primaryButton, styles3.primaryButtonDisabled] : styles3.primaryButton;
1451
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.screen, children: /* @__PURE__ */ jsxRuntime.jsxs(
1553
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.screen, children: /* @__PURE__ */ jsxRuntime.jsx(
1452
1554
  reactNative.View,
1453
1555
  {
1454
1556
  style: styles3.card,
1455
1557
  testID: withTestIdPrefix(AuthTestIds.resetPasswordForm, testIdPrefix),
1456
- children: [
1457
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.title, children: labels.title }),
1458
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.subtitle, children: labels.description }),
1459
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.fieldGroup, children: [
1460
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.label, children: labels.newPasswordLabel }),
1461
- /* @__PURE__ */ jsxRuntime.jsx(
1462
- reactNative.TextInput,
1463
- {
1464
- secureTextEntry: true,
1465
- accessibilityHint: labels.newPasswordPlaceholder,
1466
- accessibilityLabel: labels.newPasswordLabel,
1467
- autoCapitalize: "none",
1468
- autoCorrect: false,
1469
- editable: !form.isSubmitting,
1470
- placeholder: labels.newPasswordPlaceholder,
1471
- placeholderTextColor: theme.colors.textSecondary,
1472
- style: styles3.input,
1473
- testID: withTestIdPrefix(AuthTestIds.resetPasswordNewInput, testIdPrefix),
1474
- value: form.newPassword,
1475
- onChangeText: form.setNewPassword
1476
- }
1477
- )
1478
- ] }),
1479
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.fieldGroup, children: [
1480
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.label, children: labels.confirmPasswordLabel }),
1481
- /* @__PURE__ */ jsxRuntime.jsx(
1482
- reactNative.TextInput,
1483
- {
1484
- secureTextEntry: true,
1485
- accessibilityHint: labels.confirmPasswordPlaceholder,
1486
- accessibilityLabel: labels.confirmPasswordLabel,
1487
- autoCapitalize: "none",
1488
- autoCorrect: false,
1489
- editable: !form.isSubmitting,
1490
- placeholder: labels.confirmPasswordPlaceholder,
1491
- placeholderTextColor: theme.colors.textSecondary,
1492
- style: styles3.input,
1493
- testID: withTestIdPrefix(AuthTestIds.resetPasswordConfirmInput, testIdPrefix),
1494
- value: form.confirmPassword,
1495
- onChangeText: form.setConfirmPassword
1496
- }
1497
- )
1498
- ] }),
1499
- message !== null ? /* @__PURE__ */ jsxRuntime.jsx(
1500
- reactNative.Text,
1501
- {
1502
- style: styles3.errorText,
1503
- testID: withTestIdPrefix(AuthTestIds.resetPasswordError, testIdPrefix),
1504
- children: message
1505
- }
1506
- ) : null,
1507
- /* @__PURE__ */ jsxRuntime.jsx(
1508
- reactNative.TouchableOpacity,
1509
- {
1510
- accessibilityHint: labels.submit,
1511
- accessibilityLabel: form.isSubmitting ? labels.submitting : labels.submit,
1512
- accessibilityRole: "button",
1513
- disabled: form.isSubmitting,
1514
- style: submitButtonStyle,
1515
- testID: withTestIdPrefix(AuthTestIds.resetPasswordSubmitButton, testIdPrefix),
1516
- onPress: form.submit,
1517
- children: form.isSubmitting ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.colors.onPrimary, size: "small" }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.primaryButtonText, children: labels.submit })
1518
- }
1519
- )
1520
- ]
1558
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1559
+ ResetPasswordFields,
1560
+ {
1561
+ form,
1562
+ labels,
1563
+ styles: styles3,
1564
+ testIdPrefix,
1565
+ theme
1566
+ }
1567
+ )
1521
1568
  }
1522
1569
  ) });
1523
1570
  }
@@ -4146,7 +4193,8 @@ var RegisterTestIds = {
4146
4193
  confirmPasswordInput: "register-confirm-password-input",
4147
4194
  tenantNameInput: "register-tenant-name-input",
4148
4195
  submitButton: "register-submit-button",
4149
- websiteInput: "register-website"
4196
+ websiteInput: "register-website",
4197
+ checkEmail: "register-check-email"
4150
4198
  };
4151
4199
  var FIELDS = [
4152
4200
  { key: "firstName", testId: RegisterTestIds.firstNameInput, autoComplete: "name" },
@@ -4268,6 +4316,14 @@ function routeRegisterOutcome(outcome, handlers) {
4268
4316
  }
4269
4317
  }
4270
4318
 
4319
+ // src/hooks/registerAccepted.ts
4320
+ function isRegisterAccepted(result) {
4321
+ if (typeof result !== "object" || result === null) {
4322
+ return false;
4323
+ }
4324
+ return result.accepted === true;
4325
+ }
4326
+
4271
4327
  // src/hooks/registerErrors.ts
4272
4328
  var MIN_PASSWORD_LENGTH = 8;
4273
4329
  var WHITESPACE_REGEX = /\s/;
@@ -4420,6 +4476,42 @@ function expandCompactRegisterValues(values) {
4420
4476
  };
4421
4477
  }
4422
4478
 
4479
+ // src/hooks/newIdempotencyKey.ts
4480
+ var HEX_RADIX = 16;
4481
+ var HEX_BYTE_LENGTH = 2;
4482
+ var KEY_BYTES = 16;
4483
+ var fallbackSequence = 0;
4484
+ function newIdempotencyKey() {
4485
+ const cryptoApi = globalThis.crypto;
4486
+ if (typeof cryptoApi?.randomUUID === "function") {
4487
+ return cryptoApi.randomUUID();
4488
+ }
4489
+ if (typeof cryptoApi?.getRandomValues === "function") {
4490
+ const bytes = cryptoApi.getRandomValues(new Uint8Array(KEY_BYTES));
4491
+ return Array.from(bytes, (byte) => byte.toString(HEX_RADIX).padStart(HEX_BYTE_LENGTH, "0")).join("");
4492
+ }
4493
+ fallbackSequence += 1;
4494
+ return `${Date.now().toString(HEX_RADIX)}-${String(fallbackSequence)}`;
4495
+ }
4496
+
4497
+ // src/hooks/useRegisterSubmitMeta.ts
4498
+ function useRegisterSubmitMeta() {
4499
+ const openedAt = react.useRef(Date.now());
4500
+ const key = react.useRef(null);
4501
+ return react.useMemo(
4502
+ () => ({
4503
+ next: () => {
4504
+ key.current ?? (key.current = newIdempotencyKey());
4505
+ return { idempotencyKey: key.current, formElapsedMs: Date.now() - openedAt.current };
4506
+ },
4507
+ resetKey: () => {
4508
+ key.current = null;
4509
+ }
4510
+ }),
4511
+ []
4512
+ );
4513
+ }
4514
+
4423
4515
  // src/hooks/useRegisterForm.ts
4424
4516
  var EMPTY_VALUES = {
4425
4517
  firstName: "",
@@ -4431,6 +4523,9 @@ var EMPTY_VALUES = {
4431
4523
  tenantName: "",
4432
4524
  website: ""
4433
4525
  };
4526
+ function withoutPasswords(values) {
4527
+ return { ...values, password: "", confirmPassword: "" };
4528
+ }
4434
4529
  function toSubmitValues(values) {
4435
4530
  return {
4436
4531
  firstName: values.firstName,
@@ -4446,15 +4541,18 @@ function useRegisterForm({
4446
4541
  onSubmit,
4447
4542
  passwordPolicy = isRegisterPasswordStrong,
4448
4543
  onSubmitAttempt,
4449
- compact = false
4544
+ compact = false,
4545
+ sharedRegistration = false
4450
4546
  }) {
4451
4547
  const [state, setState] = react.useState(EMPTY_VALUES);
4452
4548
  const [isSubmitting, setIsSubmitting] = react.useState(false);
4453
4549
  const [error, setError] = react.useState(null);
4454
4550
  const inFlight = react.useRef(false);
4551
+ const submitMeta = useRegisterSubmitMeta();
4455
4552
  const setField = react.useCallback((field2, value) => {
4553
+ submitMeta.resetKey();
4456
4554
  setState((prev) => ({ ...prev, [field2]: value }));
4457
- }, []);
4555
+ }, [submitMeta]);
4458
4556
  const submit = react.useCallback(async () => {
4459
4557
  if (inFlight.current) {
4460
4558
  return { ok: false, error: "inFlight" /* InFlight */ };
@@ -4471,7 +4569,11 @@ function useRegisterForm({
4471
4569
  setError(null);
4472
4570
  setIsSubmitting(true);
4473
4571
  try {
4474
- const result = await onSubmit(toSubmitValues(trimmed));
4572
+ const values = toSubmitValues(trimmed);
4573
+ const result = await (sharedRegistration ? onSubmit(values, submitMeta.next()) : onSubmit(values));
4574
+ if (sharedRegistration && isRegisterAccepted(result)) {
4575
+ setState(withoutPasswords);
4576
+ }
4475
4577
  return { ok: true, result };
4476
4578
  } catch (caught) {
4477
4579
  const code = mapRegisterErrorCode(caught);
@@ -4481,10 +4583,10 @@ function useRegisterForm({
4481
4583
  inFlight.current = false;
4482
4584
  setIsSubmitting(false);
4483
4585
  }
4484
- }, [compact, onSubmit, onSubmitAttempt, passwordPolicy, state]);
4586
+ }, [compact, onSubmit, onSubmitAttempt, passwordPolicy, sharedRegistration, state, submitMeta]);
4485
4587
  return { state, setField, submit, isSubmitting, error };
4486
4588
  }
4487
- function RegisterForm({
4589
+ function RegisterFormCore({
4488
4590
  onSubmit,
4489
4591
  onSuccess,
4490
4592
  onError,
@@ -4494,7 +4596,8 @@ function RegisterForm({
4494
4596
  theme: themeProp,
4495
4597
  footer,
4496
4598
  testIdPrefix,
4497
- compact = false
4599
+ compact = false,
4600
+ sharedRegistration
4498
4601
  }) {
4499
4602
  const theme = useAuthTheme(themeProp);
4500
4603
  const styles3 = useAuthStyles(theme);
@@ -4506,7 +4609,8 @@ function RegisterForm({
4506
4609
  onSubmit,
4507
4610
  onSubmitAttempt,
4508
4611
  passwordPolicy,
4509
- compact
4612
+ compact,
4613
+ sharedRegistration
4510
4614
  });
4511
4615
  const handlePress = react.useCallback(async () => {
4512
4616
  routeRegisterOutcome(await submit(), { onSuccess, onError });
@@ -4544,6 +4648,9 @@ function RegisterForm({
4544
4648
  footer
4545
4649
  ] }) });
4546
4650
  }
4651
+ function RegisterForm(props) {
4652
+ return /* @__PURE__ */ jsxRuntime.jsx(RegisterFormCore, { ...props, sharedRegistration: false });
4653
+ }
4547
4654
  async function lazyFetchHttpClient(request) {
4548
4655
  const fetchImpl = typeof fetch === "function" ? fetch.bind(globalThis) : void 0;
4549
4656
  if (fetchImpl === void 0) {
@@ -4772,19 +4879,155 @@ var VerifyEmailErrorCode = /* @__PURE__ */ ((VerifyEmailErrorCode2) => {
4772
4879
  VerifyEmailErrorCode2["TokenUsed"] = "TOKEN_USED";
4773
4880
  VerifyEmailErrorCode2["KeycloakUpdateFailed"] = "KEYCLOAK_UPDATE_FAILED";
4774
4881
  VerifyEmailErrorCode2["MissingToken"] = "MISSING_TOKEN";
4882
+ VerifyEmailErrorCode2["NewPasswordRequired"] = "NEW_PASSWORD_REQUIRED";
4883
+ VerifyEmailErrorCode2["WeakPassword"] = "WEAK_PASSWORD";
4884
+ VerifyEmailErrorCode2["RateLimited"] = "RATE_LIMITED";
4885
+ VerifyEmailErrorCode2["SignInFailed"] = "SIGN_IN_FAILED";
4775
4886
  VerifyEmailErrorCode2["Generic"] = "GENERIC";
4776
4887
  return VerifyEmailErrorCode2;
4777
4888
  })(VerifyEmailErrorCode || {});
4778
4889
 
4890
+ // src/verify/useVerifyNewPasswordForm.ts
4891
+ var GENERIC_FAILURE = { success: false, errorCode: "GENERIC" /* Generic */ };
4892
+ var DEAD_LINK_CODES = [
4893
+ "TOKEN_INVALID" /* TokenInvalid */,
4894
+ "TOKEN_EXPIRED" /* TokenExpired */,
4895
+ "TOKEN_USED" /* TokenUsed */,
4896
+ "SIGN_IN_FAILED" /* SignInFailed */
4897
+ ];
4898
+ function retryableError(code) {
4899
+ if (DEAD_LINK_CODES.includes(code)) {
4900
+ return null;
4901
+ }
4902
+ if (code === "WEAK_PASSWORD" /* WeakPassword */) {
4903
+ return "weakPassword" /* WeakPassword */;
4904
+ }
4905
+ return code === "RATE_LIMITED" /* RateLimited */ ? "rateLimited" /* RateLimited */ : "network" /* Network */;
4906
+ }
4907
+ function useVerifyNewPasswordForm({
4908
+ client,
4909
+ token,
4910
+ onSettled
4911
+ }) {
4912
+ const { state, setters } = useFormState();
4913
+ const [isSubmitting, setIsSubmitting] = react.useState(false);
4914
+ const inFlight = react.useRef(false);
4915
+ const send = react.useMemo(
4916
+ () => async (newPassword) => {
4917
+ if (inFlight.current) {
4918
+ return;
4919
+ }
4920
+ inFlight.current = true;
4921
+ setIsSubmitting(true);
4922
+ const result = await (client.setNewPassword?.(token, newPassword) ?? Promise.resolve(GENERIC_FAILURE)).catch(
4923
+ () => GENERIC_FAILURE
4924
+ );
4925
+ inFlight.current = false;
4926
+ setIsSubmitting(false);
4927
+ const formError = result.success ? null : retryableError(result.errorCode);
4928
+ if (formError === null) {
4929
+ onSettled(result);
4930
+ return;
4931
+ }
4932
+ setters.setErrorKey(formError);
4933
+ },
4934
+ [client, onSettled, setters, token]
4935
+ );
4936
+ return {
4937
+ ...state,
4938
+ setNewPassword: setters.setNewPassword,
4939
+ setConfirmPassword: setters.setConfirmPassword,
4940
+ isSubmitting,
4941
+ submit: () => runSubmit({
4942
+ newPassword: state.newPassword,
4943
+ confirmPassword: state.confirmPassword,
4944
+ token,
4945
+ setErrorKey: setters.setErrorKey,
4946
+ setHasInvalidToken: setters.setHasInvalidToken,
4947
+ mutate: (request) => {
4948
+ void send(request.newPassword);
4949
+ }
4950
+ })
4951
+ };
4952
+ }
4953
+ function VerifyNewPasswordStep({
4954
+ client,
4955
+ token,
4956
+ labels: labelsProp,
4957
+ styles: styles3,
4958
+ theme,
4959
+ testIdPrefix,
4960
+ onSettled
4961
+ }) {
4962
+ const labels = react.useMemo(
4963
+ () => ({ ...DEFAULT_VERIFY_NEW_PASSWORD_LABELS, ...labelsProp }),
4964
+ [labelsProp]
4965
+ );
4966
+ const form = useVerifyNewPasswordForm({ client, token, onSettled });
4967
+ const handleSubmitEditing = useSubmitOnEnter(form.submit);
4968
+ const newPasswordRef = react.useRef(null);
4969
+ useFocusOnMount(newPasswordRef);
4970
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: withTestIdPrefix(AuthTestIds.resetPasswordForm, testIdPrefix), children: /* @__PURE__ */ jsxRuntime.jsx(
4971
+ ResetPasswordFields,
4972
+ {
4973
+ form,
4974
+ labels,
4975
+ newPasswordRef,
4976
+ styles: styles3,
4977
+ testIdPrefix,
4978
+ theme,
4979
+ onSubmitEditing: handleSubmitEditing
4980
+ }
4981
+ ) });
4982
+ }
4983
+ function VerifySignInPrompt({
4984
+ labels: labelsProp,
4985
+ styles: styles3,
4986
+ testID,
4987
+ actionTestID,
4988
+ onContinue
4989
+ }) {
4990
+ const labels = react.useMemo(() => ({ ...DEFAULT_VERIFY_SIGN_IN_LABELS, ...labelsProp }), [labelsProp]);
4991
+ const actionRef = react.useRef(null);
4992
+ useFocusOnMount(actionRef);
4993
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { testID, children: [
4994
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityRole: "header", style: styles3.title, children: labels.title }),
4995
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.subtitle, children: labels.body }),
4996
+ /* @__PURE__ */ jsxRuntime.jsx(
4997
+ reactNative.TouchableOpacity,
4998
+ {
4999
+ ref: actionRef,
5000
+ accessibilityHint: labels.actionHint,
5001
+ accessibilityLabel: labels.action,
5002
+ accessibilityRole: "button",
5003
+ style: styles3.primaryButton,
5004
+ testID: actionTestID,
5005
+ onPress: onContinue,
5006
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.primaryButtonText, children: labels.action })
5007
+ }
5008
+ )
5009
+ ] });
5010
+ }
5011
+
4779
5012
  // src/verify/VerifyEmailPhase.ts
4780
5013
  var VerifyEmailPhase = /* @__PURE__ */ ((VerifyEmailPhase2) => {
4781
5014
  VerifyEmailPhase2["Loading"] = "loading";
4782
5015
  VerifyEmailPhase2["Success"] = "success";
4783
5016
  VerifyEmailPhase2["Failure"] = "failure";
5017
+ VerifyEmailPhase2["NewPasswordRequired"] = "newPasswordRequired";
4784
5018
  return VerifyEmailPhase2;
4785
5019
  })(VerifyEmailPhase || {});
4786
5020
 
4787
- // src/verify/useVerifyEmail.ts
5021
+ // src/verify/resolveVerifyView.ts
5022
+ function resolveVerifyView(verify, passwordOutcome) {
5023
+ if (passwordOutcome === null) {
5024
+ return verify;
5025
+ }
5026
+ if (passwordOutcome.success) {
5027
+ return { phase: "success" /* Success */, errorCode: "GENERIC" /* Generic */ };
5028
+ }
5029
+ return { phase: "failure" /* Failure */, errorCode: passwordOutcome.errorCode };
5030
+ }
4788
5031
  var LOADING = {
4789
5032
  phase: "loading" /* Loading */,
4790
5033
  errorCode: "GENERIC" /* Generic */
@@ -4793,6 +5036,10 @@ var SUCCESS = {
4793
5036
  phase: "success" /* Success */,
4794
5037
  errorCode: "GENERIC" /* Generic */
4795
5038
  };
5039
+ var NEW_PASSWORD_REQUIRED = {
5040
+ phase: "newPasswordRequired" /* NewPasswordRequired */,
5041
+ errorCode: "GENERIC" /* Generic */
5042
+ };
4796
5043
  var REJECTED = { success: false, errorCode: "GENERIC" /* Generic */ };
4797
5044
  function failure(errorCode) {
4798
5045
  return { phase: "failure" /* Failure */, errorCode };
@@ -4818,6 +5065,10 @@ function useVerifyEmail({ client, token, onVerified, onFailed }) {
4818
5065
  latest.current.onVerified?.();
4819
5066
  return;
4820
5067
  }
5068
+ if (result.errorCode === "NEW_PASSWORD_REQUIRED" /* NewPasswordRequired */) {
5069
+ setState(NEW_PASSWORD_REQUIRED);
5070
+ return;
5071
+ }
4821
5072
  setState(failure(result.errorCode));
4822
5073
  latest.current.onFailed?.(result.errorCode);
4823
5074
  };
@@ -4843,6 +5094,12 @@ function verifyEmailErrorText(code, labels) {
4843
5094
  if (code === "MISSING_TOKEN" /* MissingToken */) {
4844
5095
  return labels.missingToken;
4845
5096
  }
5097
+ if (code === "SIGN_IN_FAILED" /* SignInFailed */) {
5098
+ return labels.signIn?.body ?? DEFAULT_VERIFY_SIGN_IN_LABELS.body;
5099
+ }
5100
+ if (code === "RATE_LIMITED" /* RateLimited */) {
5101
+ return labels.errorRateLimited ?? labels.errorGeneric;
5102
+ }
4846
5103
  return labels.errorGeneric;
4847
5104
  }
4848
5105
  function ResendCta({ label, hint, style, textStyle, testID, onPress }) {
@@ -4879,7 +5136,20 @@ function VerifyEmailScreen({
4879
5136
  () => ({ ...DEFAULT_VERIFY_EMAIL_LABELS, ...labelsProp }),
4880
5137
  [labelsProp]
4881
5138
  );
4882
- const { phase, errorCode } = useVerifyEmail({ client, token, onVerified, onFailed });
5139
+ const verify = useVerifyEmail({ client, token, onVerified, onFailed });
5140
+ const [passwordOutcome, setPasswordOutcome] = react.useState(null);
5141
+ const { phase, errorCode } = resolveVerifyView(verify, passwordOutcome);
5142
+ const settlePassword = react.useCallback(
5143
+ (result) => {
5144
+ setPasswordOutcome(result);
5145
+ if (result.success) {
5146
+ onVerified?.();
5147
+ return;
5148
+ }
5149
+ onFailed?.(result.errorCode);
5150
+ },
5151
+ [onFailed, onVerified]
5152
+ );
4883
5153
  const [resendOpen, setResendOpen] = react.useState(false);
4884
5154
  const openResend = react.useCallback(() => setResendOpen(true), []);
4885
5155
  const id = (base) => withTestIdPrefix(base, testIdPrefix);
@@ -4889,6 +5159,18 @@ function VerifyEmailScreen({
4889
5159
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityRole: "header", style: styles3.title, children: labels.loadingTitle }),
4890
5160
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.subtitle, children: labels.loadingHint })
4891
5161
  ] }) : null,
5162
+ phase === "newPasswordRequired" /* NewPasswordRequired */ ? /* @__PURE__ */ jsxRuntime.jsx(
5163
+ VerifyNewPasswordStep,
5164
+ {
5165
+ client,
5166
+ labels: labels.newPassword,
5167
+ styles: styles3,
5168
+ testIdPrefix,
5169
+ theme,
5170
+ token,
5171
+ onSettled: settlePassword
5172
+ }
5173
+ ) : null,
4892
5174
  phase === "success" /* Success */ ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { testID: id(AuthTestIds.verifyEmailSuccess), children: [
4893
5175
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityRole: "header", style: styles3.title, children: labels.successTitle }),
4894
5176
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.subtitle, children: labels.successBody }),
@@ -4905,7 +5187,17 @@ function VerifyEmailScreen({
4905
5187
  }
4906
5188
  )
4907
5189
  ] }) : null,
4908
- phase === "failure" /* Failure */ ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { testID: id(AuthTestIds.verifyEmailError), children: [
5190
+ phase === "failure" /* Failure */ && errorCode === "SIGN_IN_FAILED" /* SignInFailed */ ? /* @__PURE__ */ jsxRuntime.jsx(
5191
+ VerifySignInPrompt,
5192
+ {
5193
+ actionTestID: id(AuthTestIds.verifyEmailSignInContinue),
5194
+ labels: labels.signIn,
5195
+ styles: styles3,
5196
+ testID: id(AuthTestIds.verifyEmailSignIn),
5197
+ onContinue
5198
+ }
5199
+ ) : null,
5200
+ phase === "failure" /* Failure */ && errorCode !== "SIGN_IN_FAILED" /* SignInFailed */ ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { testID: id(AuthTestIds.verifyEmailError), children: [
4909
5201
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityRole: "header", style: styles3.title, children: labels.errorTitle }),
4910
5202
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.errorBody, children: verifyEmailErrorText(errorCode, labels) }),
4911
5203
  resendOpen ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -4943,7 +5235,7 @@ var SERVER_ERROR_CODES2 = [
4943
5235
  "TOKEN_USED" /* TokenUsed */,
4944
5236
  "KEYCLOAK_UPDATE_FAILED" /* KeycloakUpdateFailed */
4945
5237
  ];
4946
- var GENERIC_FAILURE = { success: false, errorCode: "GENERIC" /* Generic */ };
5238
+ var GENERIC_FAILURE2 = { success: false, errorCode: "GENERIC" /* Generic */ };
4947
5239
  var INVALID_TOKEN = { success: false, errorCode: "TOKEN_INVALID" /* TokenInvalid */ };
4948
5240
  var HTTP_BAD_REQUEST3 = 400;
4949
5241
  var TOKEN_FIELD = "token";
@@ -4968,7 +5260,7 @@ function isTokenValidationRejection(status, body) {
4968
5260
  }
4969
5261
  function decodeVerifyResponse(status, body) {
4970
5262
  if (!isRecord3(body)) {
4971
- return GENERIC_FAILURE;
5263
+ return GENERIC_FAILURE2;
4972
5264
  }
4973
5265
  if (body.success === true) {
4974
5266
  return { success: true };
@@ -4976,7 +5268,7 @@ function decodeVerifyResponse(status, body) {
4976
5268
  if (isServerErrorCode(body.errorCode)) {
4977
5269
  return { success: false, errorCode: body.errorCode };
4978
5270
  }
4979
- return isTokenValidationRejection(status, body) ? INVALID_TOKEN : GENERIC_FAILURE;
5271
+ return isTokenValidationRejection(status, body) ? INVALID_TOKEN : GENERIC_FAILURE2;
4980
5272
  }
4981
5273
  function createBffVerifyEmailClient(options = {}) {
4982
5274
  const http = options.http ?? lazyFetchHttpClient3;
@@ -4994,7 +5286,7 @@ function createBffVerifyEmailClient(options = {}) {
4994
5286
  const response = await post(VERIFY_EMAIL_ENDPOINT, { token });
4995
5287
  return decodeVerifyResponse(response.status, response.data);
4996
5288
  } catch {
4997
- return GENERIC_FAILURE;
5289
+ return GENERIC_FAILURE2;
4998
5290
  }
4999
5291
  },
5000
5292
  resendVerification: async (email, verifyUrlTemplate) => {
@@ -5034,6 +5326,122 @@ function readVerifyToken(raw) {
5034
5326
  return typeof first === "string" ? first : "";
5035
5327
  }
5036
5328
 
5329
+ // src/verify/sharedRegistrationClient.ts
5330
+ var REGISTER_ENDPOINT = "/bff/register";
5331
+ var VERIFY_AND_LOGIN_ENDPOINT = "/bff/verify-and-login";
5332
+ var HTTP_ACCEPTED = 202;
5333
+ var HTTP_UNAUTHORIZED2 = 401;
5334
+ var HTTP_TOO_MANY_REQUESTS2 = 429;
5335
+ var NO_MESSAGE = "";
5336
+ var GENERIC_FAILURE3 = { success: false, errorCode: "GENERIC" /* Generic */ };
5337
+ function decodeVerifyAndLogin(response) {
5338
+ if (response.ok) {
5339
+ return { success: true };
5340
+ }
5341
+ if (response.status === HTTP_TOO_MANY_REQUESTS2) {
5342
+ return { success: false, errorCode: "RATE_LIMITED" /* RateLimited */ };
5343
+ }
5344
+ if (response.status === HTTP_UNAUTHORIZED2) {
5345
+ return { success: false, errorCode: "SIGN_IN_FAILED" /* SignInFailed */ };
5346
+ }
5347
+ const body = isRecord3(response.data) ? response.data : {};
5348
+ if (body.errorCode === "NEW_PASSWORD_REQUIRED" /* NewPasswordRequired */ || body.errorCode === "WEAK_PASSWORD" /* WeakPassword */) {
5349
+ return { success: false, errorCode: body.errorCode };
5350
+ }
5351
+ return decodeVerifyResponse(response.status, response.data);
5352
+ }
5353
+ function registerFailure(response) {
5354
+ const body = isRecord3(response.data) ? response.data : {};
5355
+ const errorCode = body.errorCode ?? body.code;
5356
+ const message = `Registration failed with status ${String(response.status)}`;
5357
+ return Object.assign(new Error(message), { status: response.status, errorCode });
5358
+ }
5359
+ function createSharedRegistrationClient(options = {}) {
5360
+ const http = options.http ?? lazyFetchHttpClient3;
5361
+ const baseUrl = options.baseUrl ?? "";
5362
+ const post = (path, body, headers = {}) => http({
5363
+ url: `${baseUrl}${path}`,
5364
+ method: "POST",
5365
+ headers: { "Content-Type": "application/json", "X-BFF-Csrf": "1", ...headers },
5366
+ body: JSON.stringify(body),
5367
+ credentials: "include"
5368
+ });
5369
+ const verifyAndLogin = async (body) => {
5370
+ try {
5371
+ return decodeVerifyAndLogin(await post(VERIFY_AND_LOGIN_ENDPOINT, body));
5372
+ } catch {
5373
+ return GENERIC_FAILURE3;
5374
+ }
5375
+ };
5376
+ const verifyEmailClient = createBffVerifyEmailClient(options);
5377
+ return {
5378
+ register: async (request, meta) => {
5379
+ const body = meta === void 0 ? request : { ...request, formElapsedMs: meta.formElapsedMs };
5380
+ const headers = meta === void 0 ? {} : { "Idempotency-Key": meta.idempotencyKey };
5381
+ const response = await post(REGISTER_ENDPOINT, body, headers).catch(() => {
5382
+ throw new Error(NO_MESSAGE);
5383
+ });
5384
+ if (!response.ok) {
5385
+ throw registerFailure(response);
5386
+ }
5387
+ return { accepted: response.status === HTTP_ACCEPTED, body: response.data };
5388
+ },
5389
+ verifyEmail: (token) => verifyAndLogin({ token }),
5390
+ setNewPassword: (token, newPassword) => verifyAndLogin({ token, newPassword }),
5391
+ resendVerification: (email, verifyUrlTemplate) => verifyEmailClient.resendVerification(email, verifyUrlTemplate)
5392
+ };
5393
+ }
5394
+ function RegisterCheckEmail({
5395
+ labels,
5396
+ styles: styles3,
5397
+ footer,
5398
+ testIdPrefix
5399
+ }) {
5400
+ const headingRef = react.useRef(null);
5401
+ useFocusOnMount(headingRef);
5402
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.screen, children: /* @__PURE__ */ jsxRuntime.jsxs(
5403
+ reactNative.View,
5404
+ {
5405
+ accessibilityLiveRegion: "polite",
5406
+ style: styles3.card,
5407
+ testID: withTestIdPrefix(RegisterTestIds.checkEmail, testIdPrefix),
5408
+ children: [
5409
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ref: headingRef, accessible: true, focusable: true, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityRole: "header", style: styles3.title, children: labels.checkEmailTitle }) }),
5410
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles3.subtitle, children: labels.checkEmailBody }),
5411
+ footer
5412
+ ]
5413
+ }
5414
+ ) });
5415
+ }
5416
+ function SharedRegisterForm({
5417
+ onSuccess,
5418
+ onAccepted,
5419
+ ...formProps
5420
+ }) {
5421
+ const { footer, labels: labelsProp, testIdPrefix, theme: themeProp } = formProps;
5422
+ const styles3 = useAuthStyles(useAuthTheme(themeProp));
5423
+ const labels = react.useMemo(
5424
+ () => ({ ...DEFAULT_REGISTER_LABELS, ...labelsProp }),
5425
+ [labelsProp]
5426
+ );
5427
+ const [accepted, setAccepted] = react.useState(false);
5428
+ const handleResult = react.useCallback(
5429
+ (result) => {
5430
+ if (!isRegisterAccepted(result)) {
5431
+ onSuccess(result);
5432
+ return;
5433
+ }
5434
+ setAccepted(true);
5435
+ onAccepted?.(result);
5436
+ },
5437
+ [onAccepted, onSuccess]
5438
+ );
5439
+ if (accepted) {
5440
+ return /* @__PURE__ */ jsxRuntime.jsx(RegisterCheckEmail, { footer, labels, styles: styles3, testIdPrefix });
5441
+ }
5442
+ return /* @__PURE__ */ jsxRuntime.jsx(RegisterFormCore, { ...formProps, sharedRegistration: true, onSuccess: handleResult });
5443
+ }
5444
+
5037
5445
  Object.defineProperty(exports, "BffAuthClient", {
5038
5446
  enumerable: true,
5039
5447
  get: function () { return authClient.BffAuthClient; }
@@ -5068,6 +5476,8 @@ exports.DEFAULT_PREFERRED_METHOD_SETTINGS_LABELS = DEFAULT_PREFERRED_METHOD_SETT
5068
5476
  exports.DEFAULT_REGISTER_LABELS = DEFAULT_REGISTER_LABELS;
5069
5477
  exports.DEFAULT_RESET_PASSWORD_LABELS = DEFAULT_RESET_PASSWORD_LABELS;
5070
5478
  exports.DEFAULT_VERIFY_EMAIL_LABELS = DEFAULT_VERIFY_EMAIL_LABELS;
5479
+ exports.DEFAULT_VERIFY_NEW_PASSWORD_LABELS = DEFAULT_VERIFY_NEW_PASSWORD_LABELS;
5480
+ exports.DEFAULT_VERIFY_SIGN_IN_LABELS = DEFAULT_VERIFY_SIGN_IN_LABELS;
5071
5481
  exports.DEVICE_PIN_ALLOWED_DIGITS = DEVICE_PIN_ALLOWED_DIGITS;
5072
5482
  exports.DEVICE_PIN_DEFAULT_DIGITS = DEVICE_PIN_DEFAULT_DIGITS;
5073
5483
  exports.DemoCredentialsHint = DemoCredentialsHint;
@@ -5096,12 +5506,15 @@ exports.PinForm = PinForm;
5096
5506
  exports.PreferredMethodHint = PreferredMethodHint;
5097
5507
  exports.PreferredMethodSettingsCard = PreferredMethodSettingsCard;
5098
5508
  exports.PrefillIntent = PrefillIntent;
5509
+ exports.REGISTER_ENDPOINT = REGISTER_ENDPOINT;
5099
5510
  exports.RESEND_VERIFICATION_ENDPOINT = RESEND_VERIFICATION_ENDPOINT;
5100
5511
  exports.RegisterErrorCode = RegisterErrorCode;
5101
5512
  exports.RegisterForm = RegisterForm;
5102
5513
  exports.RegisterTestIds = RegisterTestIds;
5103
5514
  exports.ResetPasswordError = ResetPasswordError;
5104
5515
  exports.ResetPasswordForm = ResetPasswordForm;
5516
+ exports.SharedRegisterForm = SharedRegisterForm;
5517
+ exports.VERIFY_AND_LOGIN_ENDPOINT = VERIFY_AND_LOGIN_ENDPOINT;
5105
5518
  exports.VERIFY_EMAIL_ENDPOINT = VERIFY_EMAIL_ENDPOINT;
5106
5519
  exports.VERIFY_TOKEN_PLACEHOLDER = VERIFY_TOKEN_PLACEHOLDER;
5107
5520
  exports.VerifyEmailErrorCode = VerifyEmailErrorCode;
@@ -5115,10 +5528,12 @@ exports.createBffAuthClient = createBffAuthClient;
5115
5528
  exports.createBffChangePasswordClient = createBffChangePasswordClient;
5116
5529
  exports.createBffVerifyEmailClient = createBffVerifyEmailClient;
5117
5530
  exports.createPreferencesClient = createPreferencesClient;
5531
+ exports.createSharedRegistrationClient = createSharedRegistrationClient;
5118
5532
  exports.defaultAuthTheme = defaultAuthTheme;
5119
5533
  exports.deriveUsernameFromEmail = deriveUsernameFromEmail;
5120
5534
  exports.isAllowedPinDigits = isAllowedPinDigits;
5121
5535
  exports.isPasswordValid = isPasswordValid;
5536
+ exports.isRegisterAccepted = isRegisterAccepted;
5122
5537
  exports.isRegisterPasswordStrong = isRegisterPasswordStrong;
5123
5538
  exports.isValidForgotPasswordEmail = isValidForgotPasswordEmail;
5124
5539
  exports.performBffLogout = performBffLogout;