@passflow/react 0.0.1 → 0.2.8

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.
Files changed (108) hide show
  1. package/dist/index.cjs.js +4 -4
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.es.js +1450 -136
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/src/components/flow/index.d.ts +1 -0
  6. package/dist/src/components/flow/index.d.ts.map +1 -1
  7. package/dist/src/components/flow/passflow/index.d.ts +1 -1
  8. package/dist/src/components/flow/passflow/index.d.ts.map +1 -1
  9. package/dist/src/components/flow/two-factor-setup/index.d.ts +10 -0
  10. package/dist/src/components/flow/two-factor-setup/index.d.ts.map +1 -0
  11. package/dist/src/components/flow/two-factor-setup-magic-link/index.d.ts +35 -0
  12. package/dist/src/components/flow/two-factor-setup-magic-link/index.d.ts.map +1 -0
  13. package/dist/src/components/flow/two-factor-verify/index.d.ts +10 -0
  14. package/dist/src/components/flow/two-factor-verify/index.d.ts.map +1 -0
  15. package/dist/src/components/form/forgot-password/forgot-password.d.ts.map +1 -1
  16. package/dist/src/components/form/index.d.ts +2 -0
  17. package/dist/src/components/form/index.d.ts.map +1 -1
  18. package/dist/src/components/form/invitation-join/index.d.ts +1 -1
  19. package/dist/src/components/form/invitation-join/index.d.ts.map +1 -1
  20. package/dist/src/components/form/reset-password/index.d.ts.map +1 -1
  21. package/dist/src/components/form/signin/index.d.ts +2 -1
  22. package/dist/src/components/form/signin/index.d.ts.map +1 -1
  23. package/dist/src/components/form/signup/index.d.ts +1 -1
  24. package/dist/src/components/form/signup/index.d.ts.map +1 -1
  25. package/dist/src/components/form/two-factor-setup/index.d.ts +2 -0
  26. package/dist/src/components/form/two-factor-setup/index.d.ts.map +1 -0
  27. package/dist/src/components/form/two-factor-setup/two-factor-setup-form.d.ts +9 -0
  28. package/dist/src/components/form/two-factor-setup/two-factor-setup-form.d.ts.map +1 -0
  29. package/dist/src/components/form/two-factor-verify/index.d.ts +3 -0
  30. package/dist/src/components/form/two-factor-verify/index.d.ts.map +1 -0
  31. package/dist/src/components/form/two-factor-verify/two-factor-recovery-form.d.ts +11 -0
  32. package/dist/src/components/form/two-factor-verify/two-factor-recovery-form.d.ts.map +1 -0
  33. package/dist/src/components/form/two-factor-verify/two-factor-verify-form.d.ts +14 -0
  34. package/dist/src/components/form/two-factor-verify/two-factor-verify-form.d.ts.map +1 -0
  35. package/dist/src/components/form/verify-challenge/verify-challenge-otp-manual.d.ts.map +1 -1
  36. package/dist/src/components/form/verify-challenge/{varify-challenge-otp-redirect.d.ts → verify-challenge-otp-redirect.d.ts} +1 -1
  37. package/dist/src/components/form/verify-challenge/{varify-challenge-otp-redirect.d.ts.map → verify-challenge-otp-redirect.d.ts.map} +1 -1
  38. package/dist/src/components/form/verify-challenge/{varify-challenge-success.d.ts → verify-challenge-success.d.ts} +1 -1
  39. package/dist/src/components/form/verify-challenge/verify-challenge-success.d.ts.map +1 -0
  40. package/dist/src/components/provider/passflow-provider.d.ts.map +1 -1
  41. package/dist/src/context/passflow-context.d.ts +2 -0
  42. package/dist/src/context/passflow-context.d.ts.map +1 -1
  43. package/dist/src/context/router-context.d.ts +12 -0
  44. package/dist/src/context/router-context.d.ts.map +1 -1
  45. package/dist/src/hocs/with-error.d.ts +1 -1
  46. package/dist/src/hocs/with-error.d.ts.map +1 -1
  47. package/dist/src/hooks/index.d.ts +5 -0
  48. package/dist/src/hooks/index.d.ts.map +1 -1
  49. package/dist/src/hooks/use-app-settings.d.ts.map +1 -1
  50. package/dist/src/hooks/use-passflow-store.d.ts.map +1 -1
  51. package/dist/src/hooks/use-signin.d.ts.map +1 -1
  52. package/dist/src/hooks/use-two-factor-manage.d.ts +15 -0
  53. package/dist/src/hooks/use-two-factor-manage.d.ts.map +1 -0
  54. package/dist/src/hooks/use-two-factor-setup-magic-link.d.ts +55 -0
  55. package/dist/src/hooks/use-two-factor-setup-magic-link.d.ts.map +1 -0
  56. package/dist/src/hooks/use-two-factor-setup.d.ts +18 -0
  57. package/dist/src/hooks/use-two-factor-setup.d.ts.map +1 -0
  58. package/dist/src/hooks/use-two-factor-status.d.ts +13 -0
  59. package/dist/src/hooks/use-two-factor-status.d.ts.map +1 -0
  60. package/dist/src/hooks/use-two-factor-verify.d.ts +18 -0
  61. package/dist/src/hooks/use-two-factor-verify.d.ts.map +1 -0
  62. package/dist/src/test/setup.d.ts +1 -0
  63. package/dist/src/test/setup.d.ts.map +1 -0
  64. package/dist/src/test/utils/render.d.ts +4 -0
  65. package/dist/src/test/utils/render.d.ts.map +1 -0
  66. package/dist/src/test/utils/test-passflow.d.ts +6 -0
  67. package/dist/src/test/utils/test-passflow.d.ts.map +1 -0
  68. package/dist/src/types/index.d.ts +1 -0
  69. package/dist/src/types/index.d.ts.map +1 -1
  70. package/dist/src/types/two-factor-errors.d.ts +14 -0
  71. package/dist/src/types/two-factor-errors.d.ts.map +1 -0
  72. package/dist/src/utils/classify-two-factor-error.d.ts +13 -0
  73. package/dist/src/utils/classify-two-factor-error.d.ts.map +1 -0
  74. package/dist/src/utils/{cn/index.d.ts → cn.d.ts} +1 -1
  75. package/dist/src/utils/cn.d.ts.map +1 -0
  76. package/dist/src/utils/{get-app-version/index.d.ts → get-app-version.d.ts} +1 -1
  77. package/dist/src/utils/get-app-version.d.ts.map +1 -0
  78. package/dist/src/utils/{get-auth-methods/index.d.ts → get-auth-methods.d.ts} +1 -1
  79. package/dist/src/utils/get-auth-methods.d.ts.map +1 -0
  80. package/dist/src/utils/{get-form-labels/index.d.ts → get-form-labels.d.ts} +3 -3
  81. package/dist/src/utils/get-form-labels.d.ts.map +1 -0
  82. package/dist/src/utils/{get-url-errors/index.d.ts → get-url-errors.d.ts} +1 -1
  83. package/dist/src/utils/get-url-errors.d.ts.map +1 -0
  84. package/dist/src/utils/get-url-with-tokens.d.ts +4 -0
  85. package/dist/src/utils/get-url-with-tokens.d.ts.map +1 -0
  86. package/dist/src/utils/index.d.ts +3 -1
  87. package/dist/src/utils/index.d.ts.map +1 -1
  88. package/dist/src/utils/two-factor-loop-prevention.d.ts +40 -0
  89. package/dist/src/utils/two-factor-loop-prevention.d.ts.map +1 -0
  90. package/dist/src/utils/{undefined-on-catch/index.d.ts → undefined-on-catch.d.ts} +1 -1
  91. package/dist/src/utils/undefined-on-catch.d.ts.map +1 -0
  92. package/dist/src/utils/{url-params/index.d.ts → url-params.d.ts} +1 -1
  93. package/dist/src/utils/url-params.d.ts.map +1 -0
  94. package/dist/src/utils/{validate-url/index.d.ts → validate-url.d.ts} +1 -1
  95. package/dist/src/utils/validate-url.d.ts.map +1 -0
  96. package/dist/style.css +1 -1
  97. package/package.json +31 -31
  98. package/dist/src/components/form/verify-challenge/varify-challenge-success.d.ts.map +0 -1
  99. package/dist/src/utils/cn/index.d.ts.map +0 -1
  100. package/dist/src/utils/get-app-version/index.d.ts.map +0 -1
  101. package/dist/src/utils/get-auth-methods/index.d.ts.map +0 -1
  102. package/dist/src/utils/get-form-labels/index.d.ts.map +0 -1
  103. package/dist/src/utils/get-url-errors/index.d.ts.map +0 -1
  104. package/dist/src/utils/get-url-with-tokens/index.d.ts +0 -3
  105. package/dist/src/utils/get-url-with-tokens/index.d.ts.map +0 -1
  106. package/dist/src/utils/undefined-on-catch/index.d.ts.map +0 -1
  107. package/dist/src/utils/url-params/index.d.ts.map +0 -1
  108. package/dist/src/utils/validate-url/index.d.ts.map +0 -1
package/dist/index.es.js CHANGED
@@ -5,22 +5,22 @@ import 'dayjs';
5
5
  import clsx from 'clsx';
6
6
  import { twMerge } from 'tailwind-merge';
7
7
  import * as React from 'react';
8
- import { useState, useEffect, forwardRef, createContext, useCallback, useContext, useLayoutEffect, useRef, useMemo, useReducer } from 'react';
8
+ import { useState, useEffect, forwardRef, createContext, useCallback, useContext, useRef, useLayoutEffect, useMemo, useReducer } from 'react';
9
9
  import queryString from 'query-string';
10
10
  import { getCountryForTimezone } from 'countries-and-timezones';
11
11
  import { usePhoneInput, defaultCountries, parseCountry, FlagImage } from 'react-international-phone';
12
12
  import * as PopoverPrimitive from '@radix-ui/react-popover';
13
13
  import * as DialogPrimitive from '@radix-ui/react-dialog';
14
+ import OtpInput from 'react-otp-input';
14
15
  import { HelmetProvider, Helmet } from 'react-helmet-async';
15
16
  import { ErrorBoundary } from 'react-error-boundary';
16
17
  import { phone as phone$1 } from 'phone';
17
18
  import { useForm, Controller } from 'react-hook-form';
18
- import OtpInput from 'react-otp-input';
19
19
  import { parseToken, Passflow } from '@passflow/core';
20
20
  export * from '@passflow/core';
21
21
  import 'react-dom';
22
22
 
23
- const version = "0.0.1";
23
+ const version = "0.2.8";
24
24
 
25
25
  window.passflowReactAppVersion = () => {
26
26
  console.log(`App Version: ${version}`);
@@ -69,11 +69,14 @@ const isValidUrl = (url) => {
69
69
  }
70
70
  };
71
71
 
72
- const getUrlWithTokens = async (passflow, url) => {
72
+ const getUrlWithTokens = async (passflow, url, format = "hash") => {
73
73
  const tokens = await passflow.getTokens(false);
74
74
  if (tokens) {
75
75
  tokens.scopes = void 0;
76
76
  const tokenParams = Object.entries(tokens).filter(([_, value]) => value).map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
77
+ if (format === "hash") {
78
+ return `${url}#${tokenParams}`;
79
+ }
77
80
  return `${url}?${tokenParams}`;
78
81
  }
79
82
  return url;
@@ -243,6 +246,139 @@ const getUrlErrors = (subUrl) => {
243
246
  return { error, message: message ? decodeURIComponent(message) : null };
244
247
  };
245
248
 
249
+ const ERROR_PATTERNS = {
250
+ expired: ["2FA verification expired or not required", "verification expired", "session expired", "verification not required"],
251
+ not_enabled: ["Two-factor authentication is not enabled for this user", "2FA is not enabled", "two-factor not enabled"],
252
+ invalid_code: ["invalid code", "incorrect code", "code mismatch", "wrong code", "invalid 2fa code"]
253
+ };
254
+ function classifyTwoFactorError(errorMessage) {
255
+ const normalizedMessage = errorMessage.toLowerCase();
256
+ if (ERROR_PATTERNS.expired.some((pattern) => normalizedMessage.includes(pattern.toLowerCase()))) {
257
+ return {
258
+ type: "expired",
259
+ message: errorMessage,
260
+ isRecoverable: true,
261
+ shouldAutoRedirect: true
262
+ };
263
+ }
264
+ if (ERROR_PATTERNS.not_enabled.some((pattern) => normalizedMessage.includes(pattern.toLowerCase()))) {
265
+ return {
266
+ type: "not_enabled",
267
+ message: errorMessage,
268
+ isRecoverable: false,
269
+ shouldAutoRedirect: false
270
+ };
271
+ }
272
+ if (ERROR_PATTERNS.invalid_code.some((pattern) => normalizedMessage.includes(pattern.toLowerCase()))) {
273
+ return {
274
+ type: "invalid_code",
275
+ message: errorMessage,
276
+ isRecoverable: true,
277
+ shouldAutoRedirect: false
278
+ };
279
+ }
280
+ return {
281
+ type: "generic",
282
+ message: errorMessage,
283
+ isRecoverable: true,
284
+ shouldAutoRedirect: false
285
+ };
286
+ }
287
+ function getUserFriendlyErrorMessage(error) {
288
+ switch (error.type) {
289
+ case "expired":
290
+ return "Your session has expired. Redirecting to sign in...";
291
+ case "not_enabled":
292
+ return "Two-factor authentication is not enabled for your account. Please contact your administrator to enable 2FA.";
293
+ case "invalid_code":
294
+ return "Invalid code. Please check your authenticator app and try again.";
295
+ default:
296
+ return error.message;
297
+ }
298
+ }
299
+
300
+ const STORAGE_KEY_PREFIX = "passflow_2fa";
301
+ const REDIRECT_COUNT_KEY = `${STORAGE_KEY_PREFIX}_redirect_count`;
302
+ const LAST_ERROR_KEY = `${STORAGE_KEY_PREFIX}_last_error_type`;
303
+ const MAX_REDIRECTS = 3;
304
+ const TwoFactorLoopPrevention = {
305
+ /**
306
+ * Check if redirect should be allowed
307
+ * @returns true if redirect is safe, false if loop detected
308
+ */
309
+ canRedirect() {
310
+ try {
311
+ const count = this.getRedirectCount();
312
+ return count < MAX_REDIRECTS;
313
+ } catch {
314
+ return true;
315
+ }
316
+ },
317
+ /**
318
+ * Increment redirect counter
319
+ */
320
+ incrementRedirect() {
321
+ try {
322
+ const count = this.getRedirectCount();
323
+ sessionStorage.setItem(REDIRECT_COUNT_KEY, String(count + 1));
324
+ } catch {
325
+ }
326
+ },
327
+ /**
328
+ * Get current redirect count
329
+ */
330
+ getRedirectCount() {
331
+ try {
332
+ const value = sessionStorage.getItem(REDIRECT_COUNT_KEY);
333
+ return value ? Number.parseInt(value, 10) : 0;
334
+ } catch {
335
+ return 0;
336
+ }
337
+ },
338
+ /**
339
+ * Reset redirect counter (call on successful auth)
340
+ */
341
+ reset() {
342
+ try {
343
+ sessionStorage.removeItem(REDIRECT_COUNT_KEY);
344
+ sessionStorage.removeItem(LAST_ERROR_KEY);
345
+ } catch {
346
+ }
347
+ },
348
+ /**
349
+ * Track last error type to detect repeated errors
350
+ */
351
+ setLastErrorType(errorType) {
352
+ try {
353
+ sessionStorage.setItem(LAST_ERROR_KEY, errorType);
354
+ } catch {
355
+ }
356
+ },
357
+ /**
358
+ * Get last error type
359
+ */
360
+ getLastErrorType() {
361
+ try {
362
+ return sessionStorage.getItem(LAST_ERROR_KEY);
363
+ } catch {
364
+ return null;
365
+ }
366
+ },
367
+ /**
368
+ * Check if same error is repeating
369
+ */
370
+ isRepeatingError(errorType) {
371
+ const lastError = this.getLastErrorType();
372
+ return lastError === errorType && this.getRedirectCount() > 0;
373
+ },
374
+ /**
375
+ * Get user-friendly message when loop is detected
376
+ */
377
+ getLoopDetectedMessage() {
378
+ return "Unable to complete authentication. This may be a configuration issue. Please contact support.";
379
+ }
380
+ };
381
+
246
382
  const flags = "data:image/png;base64,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";
247
383
 
248
384
  const apple = "data:image/svg+xml,%3csvg%20width='16'%20height='16'%20viewBox='0%200%2016%2016'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M13.6167%204.77308C13.5312%204.83608%2012.0218%205.64397%2012.0218%207.44037C12.0218%209.51818%2013.9425%2010.2533%2014%2010.2715C13.9912%2010.3162%2013.6948%2011.2781%2012.9873%2012.2583C12.3564%2013.1207%2011.6976%2013.9818%2010.6952%2013.9818C9.69291%2013.9818%209.43498%2013.4287%208.27784%2013.4287C7.15021%2013.4287%206.74928%2014%205.83245%2014C4.9156%2014%204.27588%2013.2019%203.54034%2012.2218C2.68836%2011.0709%202%209.28291%202%207.58597C2%204.86409%203.86316%203.42054%205.69684%203.42054C6.67116%203.42054%207.48335%204.02821%208.09508%204.02821C8.67726%204.02821%209.58531%203.38414%2010.6937%203.38414C11.1139%203.38414%2012.6233%203.42054%2013.6167%204.77308ZM10.1676%202.23182C10.6259%201.71517%2010.9503%200.998298%2010.9503%200.281428C10.9503%200.182018%2010.9414%200.0812084%2010.9223%200C10.1764%200.0266027%209.28899%200.471848%208.75398%201.0613C8.33385%201.51495%207.94176%202.23182%207.94176%202.95849C7.94176%203.06771%207.96092%203.17691%207.9698%203.21192C8.01697%203.22032%208.09361%203.23012%208.17025%203.23012C8.83946%203.23012%209.68112%202.80448%2010.1676%202.23182Z'%20fill='black'/%3e%3c/svg%3e";
@@ -580,7 +716,9 @@ const initialState = {
580
716
  appId: void 0,
581
717
  scopes: void 0,
582
718
  createTenantForNewUser: void 0,
583
- parseQueryParams: true
719
+ parseQueryParams: true,
720
+ isDiscoveringAppId: false,
721
+ hasSettingsError: false
584
722
  };
585
723
  const PassflowContext = createContext(void 0);
586
724
  const passflowReducer = (state, action) => {
@@ -619,6 +757,18 @@ const routes = {
619
757
  },
620
758
  passkey: {
621
759
  path: "/passkeysss"
760
+ },
761
+ two_factor_verify: {
762
+ path: "/two-factor-verify"
763
+ },
764
+ two_factor_recovery: {
765
+ path: "/two-factor-recovery"
766
+ },
767
+ two_factor_setup: {
768
+ path: "/two-factor-setup"
769
+ },
770
+ two_factor_setup_magic_link: {
771
+ path: "/two-factor-setup-magic-link/:token"
622
772
  }
623
773
  };
624
774
 
@@ -646,7 +796,7 @@ const AuthProvider = ({ children }) => {
646
796
  setIsLoading(true);
647
797
  try {
648
798
  const tokens = await passflow.getTokens(doRefresh);
649
- const parsedTokens = tokens ? passflow.getParsedTokenCache() : void 0;
799
+ const parsedTokens = tokens ? passflow.getParsedTokens() : void 0;
650
800
  return {
651
801
  tokens,
652
802
  parsedTokens
@@ -697,17 +847,13 @@ const useSignIn = () => {
697
847
  else if (type === "passkey") {
698
848
  await passflow.passkeyAuthenticate(payload);
699
849
  } else {
700
- const passwordlessResponse = await passflow.passwordlessSignIn(payload);
701
- cleanup();
702
- return passwordlessResponse;
850
+ return await passflow.passwordlessSignIn(payload);
703
851
  }
704
- cleanup();
705
852
  return true;
706
853
  } catch (e) {
707
854
  setIsError(true);
708
855
  const error = e;
709
856
  setErrorMessage(error.message);
710
- cleanup();
711
857
  return false;
712
858
  } finally {
713
859
  cleanup();
@@ -852,48 +998,142 @@ const useAppSettings = () => {
852
998
  const [isError, setIsError] = useState(false);
853
999
  const [errorMessage, setErrorMessage] = useState("");
854
1000
  const [isLoading, setIsLoading] = useState(false);
1001
+ const isFetchingRef = useRef(false);
855
1002
  if (!context) {
856
1003
  throw new Error("useAppSetting must be used within an PassflowProvider");
857
1004
  }
858
1005
  const { state, dispatch } = context;
859
1006
  useLayoutEffect(() => {
860
- if (!state.appSettings) {
1007
+ if (!state.appSettings && !state.hasSettingsError && !isFetchingRef.current) {
861
1008
  const fetchAllSettings = async () => {
1009
+ isFetchingRef.current = true;
862
1010
  setIsLoading(true);
863
1011
  try {
1012
+ if (!passflow.appId && !state.isDiscoveringAppId) {
1013
+ dispatch({
1014
+ type: "SET_PASSFLOW_STATE",
1015
+ payload: {
1016
+ ...state,
1017
+ isDiscoveringAppId: true
1018
+ }
1019
+ });
1020
+ try {
1021
+ const urlsToTry = ["/settings"];
1022
+ if (passflow.url) {
1023
+ urlsToTry.push(`${passflow.url}/settings`);
1024
+ }
1025
+ let response = null;
1026
+ for (const url of urlsToTry) {
1027
+ try {
1028
+ const r = await fetch(url);
1029
+ if (r.ok) {
1030
+ response = r;
1031
+ break;
1032
+ }
1033
+ } catch {
1034
+ }
1035
+ }
1036
+ if (response?.ok) {
1037
+ const settings = await response.json();
1038
+ const discoveredAppId = settings.login_app?.app_id || settings.appId;
1039
+ if (discoveredAppId) {
1040
+ passflow.setAppId(discoveredAppId);
1041
+ const stateUpdates = {
1042
+ appId: discoveredAppId
1043
+ };
1044
+ if (!state.scopes) {
1045
+ const discoveredScopes = settings.login_app?.scopes || settings.scopes;
1046
+ if (discoveredScopes) {
1047
+ stateUpdates.scopes = discoveredScopes;
1048
+ }
1049
+ }
1050
+ if (isUndefined(state.createTenantForNewUser)) {
1051
+ const createTenant = settings.login_app?.create_tenant_for_new_user ?? settings.createTenantForNewUser;
1052
+ if (createTenant !== void 0) {
1053
+ stateUpdates.createTenantForNewUser = createTenant;
1054
+ }
1055
+ }
1056
+ dispatch({
1057
+ type: "SET_PASSFLOW_STATE",
1058
+ payload: {
1059
+ ...state,
1060
+ ...stateUpdates,
1061
+ isDiscoveringAppId: false
1062
+ }
1063
+ });
1064
+ } else {
1065
+ dispatch({
1066
+ type: "SET_PASSFLOW_STATE",
1067
+ payload: {
1068
+ ...state,
1069
+ isDiscoveringAppId: false
1070
+ }
1071
+ });
1072
+ }
1073
+ }
1074
+ } catch (discoveryError) {
1075
+ console.warn("Failed to discover appId from /settings:", discoveryError);
1076
+ dispatch({
1077
+ type: "SET_PASSFLOW_STATE",
1078
+ payload: {
1079
+ ...state,
1080
+ isDiscoveringAppId: false
1081
+ }
1082
+ });
1083
+ }
1084
+ }
864
1085
  let appSettings = {};
865
1086
  if (passflow.appId) {
866
1087
  appSettings = await passflow.getAppSettings();
867
1088
  }
868
- if (!state.scopes) state.scopes = appSettings.defaults.scopes;
869
- if (isUndefined(state.createTenantForNewUser)) state.createTenantForNewUser = appSettings.defaults.create_tenant_for_new_user;
1089
+ const finalStateUpdates = {
1090
+ appSettings
1091
+ };
1092
+ if (!state.scopes) finalStateUpdates.scopes = appSettings.defaults?.scopes;
1093
+ if (isUndefined(state.createTenantForNewUser))
1094
+ finalStateUpdates.createTenantForNewUser = appSettings.defaults?.create_tenant_for_new_user;
870
1095
  let passwordPolicy = null;
871
1096
  if (appSettings.auth_strategies && hasPasswordStrategy(appSettings.auth_strategies)) {
872
1097
  passwordPolicy = await passflow.getPasswordPolicySettings();
873
1098
  }
1099
+ finalStateUpdates.passwordPolicy = passwordPolicy;
874
1100
  dispatch({
875
1101
  type: "SET_PASSFLOW_STATE",
876
1102
  payload: {
877
1103
  ...state,
878
- appSettings,
879
- passwordPolicy
1104
+ ...finalStateUpdates
880
1105
  }
881
1106
  });
882
1107
  } catch (e) {
883
1108
  setIsError(true);
884
1109
  const error = e;
885
1110
  setErrorMessage(error.message);
1111
+ dispatch({
1112
+ type: "SET_PASSFLOW_STATE",
1113
+ payload: {
1114
+ ...state,
1115
+ hasSettingsError: true
1116
+ }
1117
+ });
886
1118
  } finally {
887
1119
  setIsLoading(false);
1120
+ isFetchingRef.current = false;
888
1121
  }
889
1122
  };
890
1123
  void fetchAllSettings();
891
1124
  }
892
- }, [dispatch, state.appSettings, passflow, state]);
1125
+ }, [dispatch, state.appSettings, state.hasSettingsError, state.isDiscoveringAppId, passflow]);
893
1126
  const reset = () => {
894
1127
  setIsError(false);
895
1128
  setErrorMessage("");
896
1129
  setIsLoading(false);
1130
+ dispatch({
1131
+ type: "SET_PASSFLOW_STATE",
1132
+ payload: {
1133
+ ...state,
1134
+ hasSettingsError: false
1135
+ }
1136
+ });
897
1137
  };
898
1138
  const applyThemeStyles = (style) => {
899
1139
  const root = document.documentElement;
@@ -1146,7 +1386,7 @@ const useNavigation = () => {
1146
1386
  setNavigate(null);
1147
1387
  return;
1148
1388
  }
1149
- const wrappedNavigate = (options) => {
1389
+ const wrappedNavigate = ((options) => {
1150
1390
  const { to, search = "", replace = false } = options;
1151
1391
  switch (router) {
1152
1392
  case "react-router":
@@ -1177,7 +1417,7 @@ const useNavigation = () => {
1177
1417
  }
1178
1418
  }
1179
1419
  }
1180
- };
1420
+ });
1181
1421
  setNavigate(wrappedNavigate);
1182
1422
  },
1183
1423
  [setNavigate, router]
@@ -1189,6 +1429,366 @@ const useNavigation = () => {
1189
1429
  };
1190
1430
  };
1191
1431
 
1432
+ const useTwoFactorStatus = () => {
1433
+ const passflow = usePassflow();
1434
+ const [data, setData] = useState(null);
1435
+ const [isLoading, setIsLoading] = useState(false);
1436
+ const [isError, setIsError] = useState(false);
1437
+ const [errorMessage, setErrorMessage] = useState("");
1438
+ const fetch = useCallback(async () => {
1439
+ setIsLoading(true);
1440
+ setIsError(false);
1441
+ setErrorMessage("");
1442
+ try {
1443
+ const response = await passflow.getTwoFactorStatus();
1444
+ setData(response);
1445
+ return response;
1446
+ } catch (e) {
1447
+ setIsError(true);
1448
+ const error = e;
1449
+ setErrorMessage(error.message);
1450
+ return null;
1451
+ } finally {
1452
+ setIsLoading(false);
1453
+ }
1454
+ }, [passflow]);
1455
+ useLayoutEffect(() => {
1456
+ void fetch();
1457
+ }, [fetch]);
1458
+ return {
1459
+ data,
1460
+ refetch: fetch,
1461
+ isLoading,
1462
+ isError,
1463
+ errorMessage
1464
+ };
1465
+ };
1466
+
1467
+ const useTwoFactorSetup = () => {
1468
+ const passflow = usePassflow();
1469
+ const [setupData, setSetupData] = useState(null);
1470
+ const [recoveryCodes, setRecoveryCodes] = useState([]);
1471
+ const [isLoading, setIsLoading] = useState(false);
1472
+ const [isError, setIsError] = useState(false);
1473
+ const [error, setError] = useState("");
1474
+ const [step, setStep] = useState("idle");
1475
+ const beginSetup = useCallback(async () => {
1476
+ setIsLoading(true);
1477
+ setIsError(false);
1478
+ setError("");
1479
+ try {
1480
+ const response = await passflow.beginTwoFactorSetup();
1481
+ setSetupData(response);
1482
+ setStep("setup");
1483
+ return response;
1484
+ } catch (e) {
1485
+ setIsError(true);
1486
+ const err = e;
1487
+ setError(err.message);
1488
+ return null;
1489
+ } finally {
1490
+ setIsLoading(false);
1491
+ }
1492
+ }, [passflow]);
1493
+ const confirmSetup = useCallback(
1494
+ async (code) => {
1495
+ setIsLoading(true);
1496
+ setIsError(false);
1497
+ setError("");
1498
+ try {
1499
+ const response = await passflow.confirmTwoFactorSetup(code);
1500
+ setRecoveryCodes(response.recovery_codes);
1501
+ setStep("complete");
1502
+ return response;
1503
+ } catch (e) {
1504
+ setIsError(true);
1505
+ const err = e;
1506
+ setError(err.message);
1507
+ return null;
1508
+ } finally {
1509
+ setIsLoading(false);
1510
+ }
1511
+ },
1512
+ [passflow]
1513
+ );
1514
+ const reset = useCallback(() => {
1515
+ setSetupData(null);
1516
+ setRecoveryCodes([]);
1517
+ setIsError(false);
1518
+ setError("");
1519
+ setStep("idle");
1520
+ }, []);
1521
+ return {
1522
+ setupData,
1523
+ recoveryCodes,
1524
+ step,
1525
+ beginSetup,
1526
+ confirmSetup,
1527
+ reset,
1528
+ isLoading,
1529
+ isError,
1530
+ error
1531
+ };
1532
+ };
1533
+
1534
+ const useTwoFactorVerify = () => {
1535
+ const passflow = usePassflow();
1536
+ const [isLoading, setIsLoading] = useState(false);
1537
+ const [isError, setIsError] = useState(false);
1538
+ const [error, setError] = useState("");
1539
+ const [errorType, setErrorType] = useState(null);
1540
+ const [errorDetails, setErrorDetails] = useState(null);
1541
+ const isVerificationRequired = useCallback(() => {
1542
+ return passflow.isTwoFactorVerificationRequired();
1543
+ }, [passflow]);
1544
+ const handleError = useCallback((e) => {
1545
+ const err = e;
1546
+ const classified = classifyTwoFactorError(err.message);
1547
+ setIsError(true);
1548
+ setError(err.message);
1549
+ setErrorType(classified.type);
1550
+ setErrorDetails(classified);
1551
+ }, []);
1552
+ const verify = useCallback(
1553
+ async (code) => {
1554
+ setIsLoading(true);
1555
+ setIsError(false);
1556
+ setError("");
1557
+ setErrorType(null);
1558
+ setErrorDetails(null);
1559
+ try {
1560
+ const response = await passflow.verifyTwoFactor(code);
1561
+ return response;
1562
+ } catch (e) {
1563
+ handleError(e);
1564
+ return null;
1565
+ } finally {
1566
+ setIsLoading(false);
1567
+ }
1568
+ },
1569
+ [passflow, handleError]
1570
+ );
1571
+ const useRecoveryCode = useCallback(
1572
+ async (code) => {
1573
+ setIsLoading(true);
1574
+ setIsError(false);
1575
+ setError("");
1576
+ setErrorType(null);
1577
+ setErrorDetails(null);
1578
+ try {
1579
+ const response = await passflow.useTwoFactorRecoveryCode(code);
1580
+ return response;
1581
+ } catch (e) {
1582
+ handleError(e);
1583
+ return null;
1584
+ } finally {
1585
+ setIsLoading(false);
1586
+ }
1587
+ },
1588
+ [passflow, handleError]
1589
+ );
1590
+ const reset = useCallback(() => {
1591
+ setIsError(false);
1592
+ setError("");
1593
+ setErrorType(null);
1594
+ setErrorDetails(null);
1595
+ }, []);
1596
+ return {
1597
+ isVerificationRequired,
1598
+ verify,
1599
+ useRecoveryCode,
1600
+ reset,
1601
+ isLoading,
1602
+ isError,
1603
+ error,
1604
+ errorType,
1605
+ errorDetails
1606
+ };
1607
+ };
1608
+
1609
+ const useTwoFactorManage = () => {
1610
+ const passflow = usePassflow();
1611
+ const [recoveryCodes, setRecoveryCodes] = useState([]);
1612
+ const [isLoading, setIsLoading] = useState(false);
1613
+ const [isError, setIsError] = useState(false);
1614
+ const [error, setError] = useState("");
1615
+ const disable = useCallback(
1616
+ async (code) => {
1617
+ setIsLoading(true);
1618
+ setIsError(false);
1619
+ setError("");
1620
+ try {
1621
+ await passflow.disableTwoFactor(code);
1622
+ return true;
1623
+ } catch (e) {
1624
+ setIsError(true);
1625
+ const err = e;
1626
+ setError(err.message);
1627
+ return false;
1628
+ } finally {
1629
+ setIsLoading(false);
1630
+ }
1631
+ },
1632
+ [passflow]
1633
+ );
1634
+ const regenerateCodes = useCallback(
1635
+ async (code) => {
1636
+ setIsLoading(true);
1637
+ setIsError(false);
1638
+ setError("");
1639
+ try {
1640
+ const response = await passflow.regenerateTwoFactorRecoveryCodes(code);
1641
+ setRecoveryCodes(response.recovery_codes);
1642
+ return response;
1643
+ } catch (e) {
1644
+ setIsError(true);
1645
+ const err = e;
1646
+ setError(err.message);
1647
+ return null;
1648
+ } finally {
1649
+ setIsLoading(false);
1650
+ }
1651
+ },
1652
+ [passflow]
1653
+ );
1654
+ const reset = useCallback(() => {
1655
+ setRecoveryCodes([]);
1656
+ setIsError(false);
1657
+ setError("");
1658
+ }, []);
1659
+ return {
1660
+ recoveryCodes,
1661
+ disable,
1662
+ regenerateCodes,
1663
+ reset,
1664
+ isLoading,
1665
+ isError,
1666
+ error
1667
+ };
1668
+ };
1669
+
1670
+ const useTwoFactorSetupMagicLink = (token) => {
1671
+ const passflow = usePassflow();
1672
+ const [isLoading, setIsLoading] = useState(true);
1673
+ const [isRetrying, setIsRetrying] = useState(false);
1674
+ const [isValidated, setIsValidated] = useState(false);
1675
+ const [error, setError] = useState(null);
1676
+ const [sessionToken, setSessionToken] = useState(null);
1677
+ const [userId, setUserId] = useState(null);
1678
+ const [appId, setAppId] = useState(null);
1679
+ const [expiresIn, setExpiresIn] = useState(null);
1680
+ const [retryCountdown, setRetryCountdown] = useState(null);
1681
+ const [validatedToken, setValidatedToken] = useState(null);
1682
+ const shouldClearOnUnmount = useRef(true);
1683
+ const countdownTimerRef = useRef(null);
1684
+ const startCountdown = useCallback((seconds) => {
1685
+ if (countdownTimerRef.current) {
1686
+ clearInterval(countdownTimerRef.current);
1687
+ }
1688
+ setRetryCountdown(seconds);
1689
+ countdownTimerRef.current = setInterval(() => {
1690
+ setRetryCountdown((prev) => {
1691
+ if (prev === null || prev <= 1) {
1692
+ if (countdownTimerRef.current) {
1693
+ clearInterval(countdownTimerRef.current);
1694
+ countdownTimerRef.current = null;
1695
+ }
1696
+ return null;
1697
+ }
1698
+ return prev - 1;
1699
+ });
1700
+ }, 1e3);
1701
+ }, []);
1702
+ const validateToken = useCallback(
1703
+ async (isRetry = false) => {
1704
+ if (!isRetry && validatedToken === token && isValidated) {
1705
+ return;
1706
+ }
1707
+ if (isRetry) {
1708
+ setIsRetrying(true);
1709
+ } else {
1710
+ setIsLoading(true);
1711
+ }
1712
+ setError(null);
1713
+ try {
1714
+ const passflowWithMagicLink = passflow;
1715
+ const response = await passflowWithMagicLink.validateTwoFactorSetupMagicLink(token);
1716
+ if (response.success && response.sessionToken && response.userId) {
1717
+ setSessionToken(response.sessionToken);
1718
+ setUserId(response.userId);
1719
+ setAppId(response.appId ?? null);
1720
+ setExpiresIn(response.expiresIn ?? null);
1721
+ setIsValidated(true);
1722
+ setValidatedToken(token);
1723
+ setRetryCountdown(null);
1724
+ shouldClearOnUnmount.current = false;
1725
+ } else if (response.error) {
1726
+ setError(response.error);
1727
+ setIsValidated(false);
1728
+ shouldClearOnUnmount.current = true;
1729
+ if (response.error.code === "RATE_LIMITED" && response.error.retryAfter) {
1730
+ startCountdown(response.error.retryAfter);
1731
+ }
1732
+ }
1733
+ } catch (err) {
1734
+ setError({
1735
+ code: "SERVER_ERROR",
1736
+ message: err instanceof Error ? err.message : "An unexpected error occurred"
1737
+ });
1738
+ setIsValidated(false);
1739
+ shouldClearOnUnmount.current = true;
1740
+ } finally {
1741
+ setIsLoading(false);
1742
+ setIsRetrying(false);
1743
+ }
1744
+ },
1745
+ [token, validatedToken, isValidated, passflow, startCountdown]
1746
+ );
1747
+ const retry = useCallback(async () => {
1748
+ if (retryCountdown !== null && retryCountdown > 0) {
1749
+ return;
1750
+ }
1751
+ setValidatedToken(null);
1752
+ await validateToken(true);
1753
+ }, [validateToken, retryCountdown]);
1754
+ useEffect(() => {
1755
+ if (!token) {
1756
+ setError({
1757
+ code: "INVALID_TOKEN",
1758
+ message: "No token provided"
1759
+ });
1760
+ setIsLoading(false);
1761
+ return;
1762
+ }
1763
+ if (token !== validatedToken) {
1764
+ validateToken();
1765
+ }
1766
+ }, [token, validatedToken, validateToken]);
1767
+ useEffect(() => {
1768
+ return () => {
1769
+ if (countdownTimerRef.current) {
1770
+ clearInterval(countdownTimerRef.current);
1771
+ }
1772
+ if (shouldClearOnUnmount.current) {
1773
+ const passflowWithSession = passflow;
1774
+ passflowWithSession.clearMagicLinkSession?.();
1775
+ }
1776
+ };
1777
+ }, [passflow]);
1778
+ return {
1779
+ isLoading,
1780
+ isRetrying,
1781
+ isValidated,
1782
+ error,
1783
+ sessionToken,
1784
+ userId,
1785
+ appId,
1786
+ expiresIn,
1787
+ retryCountdown,
1788
+ retry
1789
+ };
1790
+ };
1791
+
1192
1792
  const FieldPhone = ({ id, onChange, isError = false, className = "" }) => {
1193
1793
  const [show, setShow] = useState(false);
1194
1794
  const [filterValue, setFilterValue] = useState("");
@@ -1431,25 +2031,514 @@ const Wrapper = ({
1431
2031
  customLogo,
1432
2032
  removeBranding = false
1433
2033
  }) => {
1434
- return /* @__PURE__ */ jsxs(HelmetProvider, { children: [
1435
- /* @__PURE__ */ jsx(Helmet, { children: /* @__PURE__ */ jsx("style", { type: "text/css", children: customCss }) }),
1436
- /* @__PURE__ */ jsxs("div", { id: "passflow-wrapper", className: "passflow-wrapper", children: [
1437
- /* @__PURE__ */ jsxs("div", { className: cn("passflow-form-main-wrapper", className), children: [
1438
- /* @__PURE__ */ jsxs("div", { className: "passflow-form-main-container", children: [
1439
- customLogo ? /* @__PURE__ */ jsx("img", { src: customLogo, alt: "custom logo", className: "passflow-form-main-container-logo" }) : /* @__PURE__ */ jsx(Icon, { id: iconId, size: "large", type: "general" }),
1440
- title && /* @__PURE__ */ jsxs("div", { className: "passflow-form-header", children: [
1441
- /* @__PURE__ */ jsx("h2", { className: "passflow-form-title", children: title }),
1442
- subtitle && /* @__PURE__ */ jsx("span", { className: "passflow-form-subtitle", children: subtitle })
1443
- ] })
1444
- ] }),
1445
- children
1446
- ] }),
1447
- !removeBranding && /* @__PURE__ */ jsx("div", { className: "passflow-branding", children: /* @__PURE__ */ jsxs("p", { className: "passflow-branding-text", children: [
1448
- "Secured by ",
1449
- /* @__PURE__ */ jsx("span", { className: "passflow-branding-text-secondary passflow-secondary-font", children: "PASSFLOW" })
1450
- ] }) })
1451
- ] })
1452
- ] });
2034
+ return /* @__PURE__ */ jsxs(HelmetProvider, { children: [
2035
+ /* @__PURE__ */ jsx(Helmet, { children: /* @__PURE__ */ jsx("style", { type: "text/css", children: customCss }) }),
2036
+ /* @__PURE__ */ jsxs("div", { id: "passflow-wrapper", className: "passflow-wrapper", children: [
2037
+ /* @__PURE__ */ jsxs("div", { className: cn("passflow-form-main-wrapper", className), children: [
2038
+ /* @__PURE__ */ jsxs("div", { className: "passflow-form-main-container", children: [
2039
+ customLogo ? /* @__PURE__ */ jsx("img", { src: customLogo, alt: "custom logo", className: "passflow-form-main-container-logo" }) : /* @__PURE__ */ jsx(Icon, { id: iconId, size: "large", type: "general" }),
2040
+ title && /* @__PURE__ */ jsxs("div", { className: "passflow-form-header", children: [
2041
+ /* @__PURE__ */ jsx("h2", { className: "passflow-form-title", children: title }),
2042
+ subtitle && /* @__PURE__ */ jsx("span", { className: "passflow-form-subtitle", children: subtitle })
2043
+ ] })
2044
+ ] }),
2045
+ children
2046
+ ] }),
2047
+ !removeBranding && /* @__PURE__ */ jsx("div", { className: "passflow-branding", children: /* @__PURE__ */ jsxs("p", { className: "passflow-branding-text", children: [
2048
+ "Secured by ",
2049
+ /* @__PURE__ */ jsx("span", { className: "passflow-branding-text-secondary passflow-secondary-font", children: "PASSFLOW" })
2050
+ ] }) })
2051
+ ] })
2052
+ ] });
2053
+ };
2054
+
2055
+ const TwoFactorSetupForm = ({ onComplete, onCancel, numInputs }) => {
2056
+ const [code, setCode] = useState("");
2057
+ const [showSecret, setShowSecret] = useState(false);
2058
+ const { setupData, recoveryCodes, step, beginSetup, confirmSetup, reset, isLoading, isError, error } = useTwoFactorSetup();
2059
+ const totpDigits = numInputs ?? 6;
2060
+ const handleCodeChange = (value) => {
2061
+ setCode(value);
2062
+ };
2063
+ const handleConfirm = async (e) => {
2064
+ e.preventDefault();
2065
+ await confirmSetup(code);
2066
+ };
2067
+ const handleCopyRecoveryCodes = () => {
2068
+ navigator.clipboard.writeText(recoveryCodes.join("\n"));
2069
+ };
2070
+ const handleComplete = () => {
2071
+ onComplete?.(recoveryCodes);
2072
+ reset();
2073
+ };
2074
+ if (step === "idle") {
2075
+ return /* @__PURE__ */ jsx(
2076
+ Wrapper,
2077
+ {
2078
+ title: "Enable Two-Factor Authentication",
2079
+ subtitle: "Add an extra layer of security to your account",
2080
+ className: "passflow-2fa-setup",
2081
+ children: /* @__PURE__ */ jsxs("div", { className: "passflow-2fa-setup-start", children: [
2082
+ /* @__PURE__ */ jsx(Button, { size: "big", type: "button", variant: "primary", onClick: () => beginSetup(), disabled: isLoading, children: isLoading ? "Loading..." : "Get Started" }),
2083
+ onCancel && /* @__PURE__ */ jsx("button", { type: "button", className: "passflow-link-button", onClick: onCancel, children: "Cancel" }),
2084
+ isError && /* @__PURE__ */ jsxs("div", { className: "passflow-field-error", children: [
2085
+ /* @__PURE__ */ jsx(Icon, { size: "small", id: "warning", type: "general", className: "icon-warning" }),
2086
+ /* @__PURE__ */ jsx("span", { className: "passflow-field-error-text", children: error })
2087
+ ] })
2088
+ ] })
2089
+ }
2090
+ );
2091
+ }
2092
+ if (step === "setup" && setupData) {
2093
+ return /* @__PURE__ */ jsx(
2094
+ Wrapper,
2095
+ {
2096
+ title: "Scan QR Code",
2097
+ subtitle: "Scan this code with your authenticator app (Google Authenticator, Authy, etc.)",
2098
+ className: "passflow-2fa-setup",
2099
+ children: /* @__PURE__ */ jsxs("form", { onSubmit: handleConfirm, className: "passflow-2fa-setup-qr", children: [
2100
+ /* @__PURE__ */ jsx("div", { className: "passflow-2fa-qr-container", children: /* @__PURE__ */ jsx("img", { src: `data:image/png;base64,${setupData.qr_code}`, alt: "2FA QR Code", className: "passflow-2fa-qr-image" }) }),
2101
+ /* @__PURE__ */ jsxs("div", { className: "passflow-2fa-secret-toggle", children: [
2102
+ /* @__PURE__ */ jsx("button", { type: "button", className: "passflow-link-button", onClick: () => setShowSecret(!showSecret), children: showSecret ? "Hide manual code" : "Can't scan? Enter code manually" }),
2103
+ showSecret && /* @__PURE__ */ jsx("code", { className: "passflow-2fa-secret-code", children: setupData.secret })
2104
+ ] }),
2105
+ /* @__PURE__ */ jsxs("div", { className: "passflow-2fa-confirm-section", children: [
2106
+ /* @__PURE__ */ jsxs("p", { className: "passflow-2fa-confirm-label", children: [
2107
+ "Enter the ",
2108
+ totpDigits,
2109
+ "-digit code from your app to confirm:"
2110
+ ] }),
2111
+ /* @__PURE__ */ jsxs("div", { id: "otp-wrapper", className: "passflow-verify-otp-wrapper", children: [
2112
+ /* @__PURE__ */ jsx(
2113
+ OtpInput,
2114
+ {
2115
+ value: code,
2116
+ onChange: handleCodeChange,
2117
+ numInputs: totpDigits,
2118
+ shouldAutoFocus: true,
2119
+ skipDefaultStyles: true,
2120
+ containerStyle: "passflow-verify-otp-inputs",
2121
+ inputStyle: cn("passflow-field-otp passflow-field-otp--focused", isError && "passflow-field-otp--error"),
2122
+ inputType: "text",
2123
+ renderInput: (props) => /* @__PURE__ */ jsx("input", { ...props })
2124
+ }
2125
+ ),
2126
+ isError && /* @__PURE__ */ jsxs("div", { className: "passflow-verify-otp-error", children: [
2127
+ /* @__PURE__ */ jsx(Icon, { size: "small", id: "warning", type: "general", className: "icon-warning" }),
2128
+ /* @__PURE__ */ jsx("span", { className: "passflow-verify-otp-error-text", children: error })
2129
+ ] })
2130
+ ] })
2131
+ ] }),
2132
+ /* @__PURE__ */ jsx(Button, { size: "big", type: "submit", variant: "primary", disabled: code.length !== totpDigits || isLoading, children: isLoading ? "Confirming..." : "Confirm & Enable" })
2133
+ ] })
2134
+ }
2135
+ );
2136
+ }
2137
+ if (step === "complete" && recoveryCodes.length > 0) {
2138
+ return /* @__PURE__ */ jsx(
2139
+ Wrapper,
2140
+ {
2141
+ title: "Save Your Recovery Codes",
2142
+ subtitle: "Store these codes in a safe place. You'll need them if you lose access to your authenticator.",
2143
+ className: "passflow-2fa-setup",
2144
+ children: /* @__PURE__ */ jsxs("div", { className: "passflow-2fa-setup-complete", children: [
2145
+ /* @__PURE__ */ jsxs("div", { className: "passflow-2fa-warning", children: [
2146
+ /* @__PURE__ */ jsx(Icon, { size: "small", id: "warning", type: "general", className: "icon-warning" }),
2147
+ /* @__PURE__ */ jsx("span", { children: "These codes will only be shown once. Make sure to save them!" })
2148
+ ] }),
2149
+ /* @__PURE__ */ jsx("div", { className: "passflow-2fa-recovery-codes", children: recoveryCodes.map((recoveryCode) => /* @__PURE__ */ jsx("code", { className: "passflow-2fa-recovery-code", children: recoveryCode }, recoveryCode)) }),
2150
+ /* @__PURE__ */ jsx(Button, { size: "big", type: "button", variant: "secondary", onClick: handleCopyRecoveryCodes, children: "Copy to Clipboard" }),
2151
+ /* @__PURE__ */ jsx(Button, { size: "big", type: "button", variant: "primary", onClick: handleComplete, children: "I've Saved These Codes" })
2152
+ ] })
2153
+ }
2154
+ );
2155
+ }
2156
+ return null;
2157
+ };
2158
+
2159
+ const TwoFactorSetupFlow = ({
2160
+ twoFactorVerifyPath = routes.two_factor_verify.path,
2161
+ signInPath = routes.signin.path
2162
+ }) => {
2163
+ const [shouldBlockRender, setShouldBlockRender] = useState(false);
2164
+ const { navigate } = useNavigation();
2165
+ const passflow = usePassflow();
2166
+ useEffect(() => {
2167
+ if (!passflow.isTwoFactorVerificationRequired()) {
2168
+ if (TwoFactorLoopPrevention.canRedirect()) {
2169
+ TwoFactorLoopPrevention.incrementRedirect();
2170
+ navigate({ to: signInPath });
2171
+ setShouldBlockRender(true);
2172
+ } else {
2173
+ setShouldBlockRender(false);
2174
+ }
2175
+ }
2176
+ }, [passflow, navigate, signInPath]);
2177
+ const handleSetupComplete = () => {
2178
+ navigate({ to: twoFactorVerifyPath });
2179
+ };
2180
+ const handleCancel = () => {
2181
+ TwoFactorLoopPrevention.reset();
2182
+ navigate({ to: signInPath });
2183
+ };
2184
+ if (shouldBlockRender) {
2185
+ return null;
2186
+ }
2187
+ if (!passflow.isTwoFactorVerificationRequired() && TwoFactorLoopPrevention.canRedirect()) {
2188
+ return null;
2189
+ }
2190
+ return /* @__PURE__ */ jsx(TwoFactorSetupForm, { onComplete: handleSetupComplete, onCancel: handleCancel });
2191
+ };
2192
+
2193
+ const TwoFactorVerifyForm = ({
2194
+ onSuccess,
2195
+ onUseRecovery,
2196
+ title = "Two-Factor Authentication",
2197
+ subtitle,
2198
+ numInputs,
2199
+ shouldAutoFocus = true,
2200
+ signInPath,
2201
+ twoFactorSetupPath
2202
+ }) => {
2203
+ const [code, setCode] = useState("");
2204
+ const { verify, isLoading, isError, errorDetails, reset } = useTwoFactorVerify();
2205
+ const totpDigits = numInputs ?? 6;
2206
+ const computedSubtitle = subtitle ?? `Enter the ${totpDigits}-digit code from your authenticator app`;
2207
+ const handleChange = (value) => {
2208
+ reset();
2209
+ setCode(value);
2210
+ };
2211
+ const handleSubmit = async (e) => {
2212
+ e.preventDefault();
2213
+ const result = await verify(code);
2214
+ if (result) {
2215
+ onSuccess?.();
2216
+ }
2217
+ };
2218
+ const handleOtpChange = async (value) => {
2219
+ handleChange(value);
2220
+ if (value.length === totpDigits) {
2221
+ const result = await verify(value);
2222
+ if (result) {
2223
+ onSuccess?.();
2224
+ }
2225
+ }
2226
+ };
2227
+ const handleBackToSignIn = () => {
2228
+ TwoFactorLoopPrevention.reset();
2229
+ window.location.href = signInPath || "/signin";
2230
+ };
2231
+ const handleSetup2FA = () => {
2232
+ window.location.href = twoFactorSetupPath || "/two-factor-setup";
2233
+ };
2234
+ const isLoopDetected = !TwoFactorLoopPrevention.canRedirect();
2235
+ const renderErrorContent = () => {
2236
+ if (!isError || !errorDetails) return null;
2237
+ if (isLoopDetected) {
2238
+ return /* @__PURE__ */ jsxs("div", { className: "passflow-form-error passflow-form-error--critical", children: [
2239
+ /* @__PURE__ */ jsx(Icon, { size: "medium", id: "warning", type: "general", className: "icon-warning" }),
2240
+ /* @__PURE__ */ jsxs("div", { className: "passflow-form-error-content", children: [
2241
+ /* @__PURE__ */ jsx("span", { className: "passflow-form-error-text", children: TwoFactorLoopPrevention.getLoopDetectedMessage() }),
2242
+ signInPath && /* @__PURE__ */ jsx(
2243
+ Button,
2244
+ {
2245
+ type: "button",
2246
+ size: "medium",
2247
+ variant: "secondary",
2248
+ onClick: handleBackToSignIn,
2249
+ className: "passflow-button-back-to-signin",
2250
+ children: "Back to Sign In"
2251
+ }
2252
+ )
2253
+ ] })
2254
+ ] });
2255
+ }
2256
+ if (errorDetails.type === "not_enabled") {
2257
+ return /* @__PURE__ */ jsxs("div", { className: "passflow-form-error passflow-form-error--persistent", children: [
2258
+ /* @__PURE__ */ jsx(Icon, { size: "medium", id: "warning", type: "general", className: "icon-warning" }),
2259
+ /* @__PURE__ */ jsxs("div", { className: "passflow-form-error-content", children: [
2260
+ /* @__PURE__ */ jsx("span", { className: "passflow-form-error-text", children: "You need to set up two-factor authentication first." }),
2261
+ /* @__PURE__ */ jsxs("div", { className: "passflow-form-error-actions", children: [
2262
+ twoFactorSetupPath && /* @__PURE__ */ jsx(
2263
+ Button,
2264
+ {
2265
+ type: "button",
2266
+ size: "medium",
2267
+ variant: "primary",
2268
+ onClick: handleSetup2FA,
2269
+ className: "passflow-button-setup-2fa",
2270
+ children: "Set Up 2FA"
2271
+ }
2272
+ ),
2273
+ signInPath && /* @__PURE__ */ jsx(
2274
+ Button,
2275
+ {
2276
+ type: "button",
2277
+ size: "medium",
2278
+ variant: "secondary",
2279
+ onClick: handleBackToSignIn,
2280
+ className: "passflow-button-back-to-signin",
2281
+ children: "Back to Sign In"
2282
+ }
2283
+ )
2284
+ ] })
2285
+ ] })
2286
+ ] });
2287
+ }
2288
+ if (errorDetails.type === "expired") {
2289
+ return /* @__PURE__ */ jsxs("div", { className: "passflow-form-info", children: [
2290
+ /* @__PURE__ */ jsx(Icon, { size: "small", id: "info", type: "general", className: "icon-info" }),
2291
+ /* @__PURE__ */ jsx("span", { className: "passflow-form-info-text", children: getUserFriendlyErrorMessage(errorDetails) })
2292
+ ] });
2293
+ }
2294
+ return /* @__PURE__ */ jsxs("div", { className: "passflow-form-error", children: [
2295
+ /* @__PURE__ */ jsx(Icon, { size: "small", id: "warning", type: "general", className: "icon-warning" }),
2296
+ /* @__PURE__ */ jsx("span", { className: "passflow-form-error-text", children: getUserFriendlyErrorMessage(errorDetails) })
2297
+ ] });
2298
+ };
2299
+ const isInputDisabled = isLoading || errorDetails?.type === "not_enabled" || errorDetails?.type === "expired" || isLoopDetected;
2300
+ return /* @__PURE__ */ jsx(Wrapper, { title, subtitle: computedSubtitle, className: "passflow-2fa-verify", children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "passflow-form", children: [
2301
+ /* @__PURE__ */ jsxs("div", { className: "passflow-form-container", children: [
2302
+ /* @__PURE__ */ jsx("div", { className: "passflow-verify-otp-wrapper", children: /* @__PURE__ */ jsx(
2303
+ OtpInput,
2304
+ {
2305
+ value: code,
2306
+ onChange: handleOtpChange,
2307
+ numInputs: totpDigits,
2308
+ shouldAutoFocus: shouldAutoFocus && !isInputDisabled,
2309
+ skipDefaultStyles: true,
2310
+ containerStyle: "passflow-verify-otp-inputs",
2311
+ inputStyle: cn(
2312
+ "passflow-field-otp passflow-field-otp--focused",
2313
+ isError && "passflow-field-otp--error",
2314
+ isInputDisabled && "passflow-field-otp--disabled"
2315
+ ),
2316
+ inputType: "text",
2317
+ renderInput: (props) => /* @__PURE__ */ jsx("input", { ...props, disabled: isInputDisabled })
2318
+ }
2319
+ ) }),
2320
+ renderErrorContent()
2321
+ ] }),
2322
+ !isInputDisabled && /* @__PURE__ */ jsx(
2323
+ Button,
2324
+ {
2325
+ size: "big",
2326
+ type: "submit",
2327
+ variant: "primary",
2328
+ disabled: code.length !== totpDigits || isLoading,
2329
+ className: "passflow-button-signin",
2330
+ children: isLoading ? "Verifying..." : "Verify"
2331
+ }
2332
+ ),
2333
+ onUseRecovery && !isInputDisabled && /* @__PURE__ */ jsx("div", { className: "passflow-form-actions", children: /* @__PURE__ */ jsxs("p", { className: "passflow-dont-have-account", children: [
2334
+ "Lost your device?",
2335
+ " ",
2336
+ /* @__PURE__ */ jsx(
2337
+ "button",
2338
+ {
2339
+ type: "button",
2340
+ onClick: onUseRecovery,
2341
+ className: "passflow-link",
2342
+ style: { background: "none", border: "none", cursor: "pointer" },
2343
+ children: "Use recovery code"
2344
+ }
2345
+ )
2346
+ ] }) })
2347
+ ] }) });
2348
+ };
2349
+
2350
+ const TwoFactorRecoveryForm = ({
2351
+ onSuccess,
2352
+ onBack,
2353
+ title = "Use Recovery Code",
2354
+ subtitle = "Enter one of your recovery codes",
2355
+ signInPath
2356
+ }) => {
2357
+ const [code, setCode] = useState("");
2358
+ const { useRecoveryCode, isLoading, isError, errorDetails, reset } = useTwoFactorVerify();
2359
+ const handleChange = (e) => {
2360
+ reset();
2361
+ setCode(e.target.value.toUpperCase().replace(/\s/g, ""));
2362
+ };
2363
+ const handleSubmit = async (e) => {
2364
+ e.preventDefault();
2365
+ const result = await useRecoveryCode(code);
2366
+ if (result) {
2367
+ onSuccess?.();
2368
+ }
2369
+ };
2370
+ const handleBackToSignIn = () => {
2371
+ TwoFactorLoopPrevention.reset();
2372
+ window.location.href = signInPath || "/signin";
2373
+ };
2374
+ const isLoopDetected = !TwoFactorLoopPrevention.canRedirect();
2375
+ const renderErrorContent = () => {
2376
+ if (!isError || !errorDetails) return null;
2377
+ if (isLoopDetected) {
2378
+ return /* @__PURE__ */ jsxs("div", { className: "passflow-form-error passflow-form-error--critical", children: [
2379
+ /* @__PURE__ */ jsx(Icon, { size: "medium", id: "warning", type: "general", className: "icon-warning" }),
2380
+ /* @__PURE__ */ jsxs("div", { className: "passflow-form-error-content", children: [
2381
+ /* @__PURE__ */ jsx("span", { className: "passflow-form-error-text", children: TwoFactorLoopPrevention.getLoopDetectedMessage() }),
2382
+ signInPath && /* @__PURE__ */ jsx(
2383
+ Button,
2384
+ {
2385
+ type: "button",
2386
+ size: "medium",
2387
+ variant: "secondary",
2388
+ onClick: handleBackToSignIn,
2389
+ className: "passflow-button-back-to-signin",
2390
+ children: "Back to Sign In"
2391
+ }
2392
+ )
2393
+ ] })
2394
+ ] });
2395
+ }
2396
+ if (errorDetails.type === "not_enabled") {
2397
+ return /* @__PURE__ */ jsxs("div", { className: "passflow-form-error passflow-form-error--persistent", children: [
2398
+ /* @__PURE__ */ jsx(Icon, { size: "medium", id: "warning", type: "general", className: "icon-warning" }),
2399
+ /* @__PURE__ */ jsxs("div", { className: "passflow-form-error-content", children: [
2400
+ /* @__PURE__ */ jsx("span", { className: "passflow-form-error-text", children: getUserFriendlyErrorMessage(errorDetails) }),
2401
+ signInPath && /* @__PURE__ */ jsx(
2402
+ Button,
2403
+ {
2404
+ type: "button",
2405
+ size: "medium",
2406
+ variant: "secondary",
2407
+ onClick: handleBackToSignIn,
2408
+ className: "passflow-button-back-to-signin",
2409
+ children: "Back to Sign In"
2410
+ }
2411
+ )
2412
+ ] })
2413
+ ] });
2414
+ }
2415
+ if (errorDetails.type === "expired") {
2416
+ return /* @__PURE__ */ jsxs("div", { className: "passflow-form-info", children: [
2417
+ /* @__PURE__ */ jsx(Icon, { size: "small", id: "info", type: "general", className: "icon-info" }),
2418
+ /* @__PURE__ */ jsx("span", { className: "passflow-form-info-text", children: getUserFriendlyErrorMessage(errorDetails) })
2419
+ ] });
2420
+ }
2421
+ return /* @__PURE__ */ jsxs("div", { className: "passflow-form-error", children: [
2422
+ /* @__PURE__ */ jsx(Icon, { size: "small", id: "warning", type: "general", className: "icon-warning" }),
2423
+ /* @__PURE__ */ jsx("span", { className: "passflow-form-error-text", children: getUserFriendlyErrorMessage(errorDetails) })
2424
+ ] });
2425
+ };
2426
+ const isInputDisabled = isLoading || errorDetails?.type === "not_enabled" || errorDetails?.type === "expired" || isLoopDetected;
2427
+ return /* @__PURE__ */ jsx(Wrapper, { title, subtitle, className: "passflow-2fa-recovery", children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "passflow-form", children: [
2428
+ /* @__PURE__ */ jsx("div", { className: "passflow-form-container", children: /* @__PURE__ */ jsxs("div", { className: "passflow-form-field", children: [
2429
+ /* @__PURE__ */ jsx("label", { htmlFor: "recovery-code", className: cn("passflow-field-label", isError && "passflow-field-label--error"), children: "Recovery Code" }),
2430
+ /* @__PURE__ */ jsx(
2431
+ "input",
2432
+ {
2433
+ id: "recovery-code",
2434
+ type: "text",
2435
+ value: code,
2436
+ onChange: handleChange,
2437
+ placeholder: "XXXX-XXXX",
2438
+ className: cn(
2439
+ "passflow-field passflow-field--text passflow-field--focused",
2440
+ isError && "passflow-field--error",
2441
+ isInputDisabled && "passflow-field--disabled"
2442
+ ),
2443
+ autoComplete: "off",
2444
+ disabled: isInputDisabled
2445
+ }
2446
+ ),
2447
+ renderErrorContent()
2448
+ ] }) }),
2449
+ !isInputDisabled && /* @__PURE__ */ jsx(
2450
+ Button,
2451
+ {
2452
+ size: "big",
2453
+ type: "submit",
2454
+ variant: "primary",
2455
+ disabled: code.length < 8 || isLoading,
2456
+ className: "passflow-button-signin",
2457
+ children: isLoading ? "Verifying..." : "Verify"
2458
+ }
2459
+ ),
2460
+ onBack && !isInputDisabled && /* @__PURE__ */ jsx("div", { className: "passflow-form-actions", children: /* @__PURE__ */ jsxs("p", { className: "passflow-dont-have-account", children: [
2461
+ "Have your device?",
2462
+ " ",
2463
+ /* @__PURE__ */ jsx(
2464
+ "button",
2465
+ {
2466
+ type: "button",
2467
+ onClick: onBack,
2468
+ className: "passflow-link",
2469
+ style: { background: "none", border: "none", cursor: "pointer" },
2470
+ children: "Use authenticator code"
2471
+ }
2472
+ )
2473
+ ] }) })
2474
+ ] }) });
2475
+ };
2476
+
2477
+ const TwoFactorVerifyFlow = ({
2478
+ successAuthRedirect,
2479
+ signInPath = routes.signin.path,
2480
+ twoFactorSetupPath = routes.two_factor_setup?.path
2481
+ }) => {
2482
+ const [mode, setMode] = useState("verify");
2483
+ const [shouldBlockRender, setShouldBlockRender] = useState(false);
2484
+ const { navigate } = useNavigation();
2485
+ const passflow = usePassflow();
2486
+ const { errorType } = useTwoFactorVerify();
2487
+ useEffect(() => {
2488
+ if (!passflow.isTwoFactorVerificationRequired()) {
2489
+ if (TwoFactorLoopPrevention.canRedirect()) {
2490
+ TwoFactorLoopPrevention.incrementRedirect();
2491
+ navigate({ to: signInPath });
2492
+ setShouldBlockRender(true);
2493
+ } else {
2494
+ setShouldBlockRender(false);
2495
+ }
2496
+ }
2497
+ }, [passflow, navigate, signInPath]);
2498
+ useEffect(() => {
2499
+ if (errorType === "expired") {
2500
+ if (TwoFactorLoopPrevention.canRedirect()) {
2501
+ TwoFactorLoopPrevention.incrementRedirect();
2502
+ TwoFactorLoopPrevention.setLastErrorType("expired");
2503
+ setTimeout(() => {
2504
+ navigate({ to: signInPath });
2505
+ }, 1500);
2506
+ }
2507
+ } else if (errorType === "not_enabled") {
2508
+ TwoFactorLoopPrevention.setLastErrorType("not_enabled");
2509
+ }
2510
+ }, [errorType, navigate, signInPath]);
2511
+ const handleSuccess = async () => {
2512
+ TwoFactorLoopPrevention.reset();
2513
+ if (successAuthRedirect) {
2514
+ if (!isValidUrl(successAuthRedirect)) {
2515
+ navigate({ to: successAuthRedirect });
2516
+ } else {
2517
+ window.location.href = await getUrlWithTokens(passflow, successAuthRedirect);
2518
+ }
2519
+ }
2520
+ };
2521
+ const handleUseRecovery = () => {
2522
+ setMode("recovery");
2523
+ };
2524
+ const handleBackToVerify = () => {
2525
+ setMode("verify");
2526
+ };
2527
+ if (shouldBlockRender) {
2528
+ return null;
2529
+ }
2530
+ if (!passflow.isTwoFactorVerificationRequired() && TwoFactorLoopPrevention.canRedirect()) {
2531
+ return null;
2532
+ }
2533
+ return /* @__PURE__ */ jsx(Fragment, { children: mode === "verify" ? /* @__PURE__ */ jsx(
2534
+ TwoFactorVerifyForm,
2535
+ {
2536
+ onSuccess: handleSuccess,
2537
+ onUseRecovery: handleUseRecovery,
2538
+ signInPath,
2539
+ twoFactorSetupPath
2540
+ }
2541
+ ) : /* @__PURE__ */ jsx(TwoFactorRecoveryForm, { onSuccess: handleSuccess, onBack: handleBackToVerify, signInPath }) });
1453
2542
  };
1454
2543
 
1455
2544
  const defaultErrorMessage = "Something went wrong";
@@ -1479,13 +2568,25 @@ const ErrorComponent = ({ error = defaultErrorMessage, goBackRedirectTo }) => {
1479
2568
  );
1480
2569
  };
1481
2570
 
1482
- const excludeRoutes = ["verify-challenge-otp", "password/reset"];
2571
+ const excludeRoutes = ["verify-challenge-otp", "password/reset", "two-factor-verify", "two-factor-recovery"];
1483
2572
  const withError = (Component, ErrorComponent) => (props) => {
1484
2573
  const context = useContext(PassflowContext);
1485
2574
  const { successAuthRedirect } = props;
1486
2575
  const { pathname } = window.location;
1487
- if ((!context?.state.appId || !context.state.url) && !excludeRoutes.some((route) => pathname.includes(route))) {
1488
- const errorMessage = "Missing appId or url";
2576
+ if (excludeRoutes.some((route) => pathname.includes(route))) {
2577
+ return /* @__PURE__ */ jsx(
2578
+ ErrorBoundary,
2579
+ {
2580
+ FallbackComponent: ({ error }) => /* @__PURE__ */ jsx(ErrorComponent, { goBackRedirectTo: successAuthRedirect ?? "/", error: error.message }),
2581
+ children: /* @__PURE__ */ jsx(Component, { ...props })
2582
+ }
2583
+ );
2584
+ }
2585
+ if (context?.state.isDiscoveringAppId) {
2586
+ return null;
2587
+ }
2588
+ if (!context?.state.appId) {
2589
+ const errorMessage = "Missing appId";
1489
2590
  return /* @__PURE__ */ jsx(ErrorComponent, { goBackRedirectTo: successAuthRedirect ?? "/", error: errorMessage });
1490
2591
  }
1491
2592
  return /* @__PURE__ */ jsx(
@@ -1508,7 +2609,8 @@ const SignInForm = ({
1508
2609
  signUpPath = routes.signup.path,
1509
2610
  verifyOTPPath = routes.verify_otp.path,
1510
2611
  verifyMagicLinkPath = routes.verify_magic_link.path,
1511
- forgotPasswordPath = routes.forgot_password.path
2612
+ forgotPasswordPath = routes.forgot_password.path,
2613
+ twoFactorVerifyPath = routes.two_factor_verify.path
1512
2614
  }) => {
1513
2615
  const {
1514
2616
  getValues,
@@ -1523,7 +2625,16 @@ const SignInForm = ({
1523
2625
  });
1524
2626
  const passflow = usePassflow();
1525
2627
  const { navigate } = useNavigation();
1526
- const { appSettings, scopes, createTenantForNewUser, passwordPolicy, currentStyles, isError: isErrorApp, error: errorApp, loginAppTheme } = useAppSettings();
2628
+ const {
2629
+ appSettings,
2630
+ scopes,
2631
+ createTenantForNewUser,
2632
+ passwordPolicy,
2633
+ currentStyles,
2634
+ isError: isErrorApp,
2635
+ error: errorApp,
2636
+ loginAppTheme
2637
+ } = useAppSettings();
1527
2638
  const { federatedWithRedirect } = useProvider(successAuthRedirect, createTenantForNewUser);
1528
2639
  if (isErrorApp) throw new Error(errorApp);
1529
2640
  const authMethods = useMemo(() => getAuthMethods(appSettings?.auth_strategies), [appSettings]);
@@ -1576,8 +2687,13 @@ const SignInForm = ({
1576
2687
  };
1577
2688
  const status = await fetch(payload, "password");
1578
2689
  if (status) {
1579
- if (!isValidUrl(successAuthRedirect ?? appSettings.defaults.redirect)) navigate({ to: successAuthRedirect ?? appSettings.defaults.redirect });
1580
- else window.location.href = await getUrlWithTokens(passflow, successAuthRedirect ?? appSettings.defaults.redirect);
2690
+ if (passflow.isTwoFactorVerificationRequired()) {
2691
+ navigate({ to: twoFactorVerifyPath ?? routes.two_factor_verify.path });
2692
+ return;
2693
+ }
2694
+ const redirectUrl = successAuthRedirect ?? appSettings?.defaults.redirect ?? "";
2695
+ if (!isValidUrl(redirectUrl)) navigate({ to: redirectUrl });
2696
+ else window.location.href = await getUrlWithTokens(passflow, redirectUrl);
1581
2697
  }
1582
2698
  };
1583
2699
  const onSubmitPasskeyHandler = async (passkeyPayload) => {
@@ -1587,8 +2703,13 @@ const SignInForm = ({
1587
2703
  };
1588
2704
  const response = await fetch(payload, "passkey");
1589
2705
  if (response) {
1590
- if (!isValidUrl(successAuthRedirect ?? appSettings.defaults.redirect)) navigate({ to: successAuthRedirect ?? appSettings.defaults.redirect });
1591
- else window.location.href = await getUrlWithTokens(passflow, successAuthRedirect ?? appSettings.defaults.redirect);
2706
+ if (passflow.isTwoFactorVerificationRequired()) {
2707
+ navigate({ to: twoFactorVerifyPath ?? routes.two_factor_verify.path });
2708
+ return;
2709
+ }
2710
+ const redirectUrl = successAuthRedirect ?? appSettings?.defaults.redirect ?? "";
2711
+ if (!isValidUrl(redirectUrl)) navigate({ to: redirectUrl });
2712
+ else window.location.href = await getUrlWithTokens(passflow, redirectUrl);
1592
2713
  }
1593
2714
  };
1594
2715
  const onSubmitPasswordlessHandler = async (userPayload) => {
@@ -1597,8 +2718,7 @@ const SignInForm = ({
1597
2718
  ...userPayload,
1598
2719
  challenge_type: getPasswordlessData(authMethods, defaultMethod)?.challengeType,
1599
2720
  create_tenant: createTenantForNewUser,
1600
- // biome-ignore lint/style/noNonNullAssertion: <explanation>
1601
- redirect_url: successAuthRedirect ?? appSettings.defaults.redirect,
2721
+ redirect_url: successAuthRedirect ?? appSettings?.defaults.redirect,
1602
2722
  ...!isEmpty(inviteToken) && { invite_token: inviteToken }
1603
2723
  };
1604
2724
  const response = await fetch(payload, "passwordless");
@@ -1966,7 +3086,16 @@ const SignUpForm = ({
1966
3086
  });
1967
3087
  const passflow = usePassflow();
1968
3088
  const { navigate } = useNavigation();
1969
- const { appSettings, scopes, createTenantForNewUser, passwordPolicy, currentStyles, isError: isErrorApp, error: errorApp, loginAppTheme } = useAppSettings();
3089
+ const {
3090
+ appSettings,
3091
+ scopes,
3092
+ createTenantForNewUser,
3093
+ passwordPolicy,
3094
+ currentStyles,
3095
+ isError: isErrorApp,
3096
+ error: errorApp,
3097
+ loginAppTheme
3098
+ } = useAppSettings();
1970
3099
  if (isErrorApp) throw new Error(errorApp);
1971
3100
  const { federatedWithRedirect } = useProvider(successAuthRedirect, createTenantForNewUser);
1972
3101
  const authMethods = useMemo(() => getAuthMethods(appSettings?.auth_strategies), [appSettings]);
@@ -2020,23 +3149,24 @@ const SignUpForm = ({
2020
3149
  };
2021
3150
  const status = await fetch(payload, "password");
2022
3151
  if (status) {
2023
- if (!isValidUrl(successAuthRedirect ?? appSettings.defaults.redirect)) navigate({ to: successAuthRedirect ?? appSettings.defaults.redirect });
2024
- else window.location.href = await getUrlWithTokens(passflow, successAuthRedirect ?? appSettings.defaults.redirect);
3152
+ const redirectUrl = successAuthRedirect ?? appSettings?.defaults.redirect ?? "";
3153
+ if (!isValidUrl(redirectUrl)) navigate({ to: redirectUrl });
3154
+ else window.location.href = await getUrlWithTokens(passflow, redirectUrl);
2025
3155
  }
2026
3156
  };
2027
3157
  const onSubmitPasskeyHandler = async () => {
3158
+ const redirectUrl = successAuthRedirect ?? appSettings?.defaults.redirect ?? "";
2028
3159
  const payload = {
2029
3160
  relying_party_id: relyingPartyId,
2030
3161
  create_tenant: createTenantForNewUser,
2031
- // biome-ignore lint/style/noNonNullAssertion: <explanation>
2032
- redirect_url: successAuthRedirect ?? appSettings.defaults.redirect,
3162
+ redirect_url: redirectUrl,
2033
3163
  ...!isEmpty(inviteToken) && { invite_token: inviteToken },
2034
3164
  scopes
2035
3165
  };
2036
3166
  const response = await fetch(payload, "passkey");
2037
3167
  if (response) {
2038
- if (!isValidUrl(successAuthRedirect ?? appSettings.defaults.redirect)) navigate({ to: successAuthRedirect ?? appSettings.defaults.redirect });
2039
- else window.location.href = await getUrlWithTokens(passflow, successAuthRedirect ?? appSettings.defaults.redirect);
3168
+ if (!isValidUrl(redirectUrl)) navigate({ to: redirectUrl });
3169
+ else window.location.href = await getUrlWithTokens(passflow, redirectUrl);
2040
3170
  }
2041
3171
  };
2042
3172
  const onSubmitPasswordlessHandler = async (userPayload) => {
@@ -2045,8 +3175,7 @@ const SignUpForm = ({
2045
3175
  ...userPayload,
2046
3176
  challenge_type: currentChallegeType,
2047
3177
  create_tenant: createTenantForNewUser,
2048
- // biome-ignore lint/style/noNonNullAssertion: <explanation>
2049
- redirect_url: successAuthRedirect ?? appSettings.defaults.redirect,
3178
+ redirect_url: successAuthRedirect ?? appSettings?.defaults.redirect,
2050
3179
  ...!isEmpty(inviteToken) && { invite_token: inviteToken }
2051
3180
  };
2052
3181
  const response = await fetch(payload, "passwordless");
@@ -2443,66 +3572,6 @@ const VerifyChallengeMagicLink = () => {
2443
3572
  );
2444
3573
  };
2445
3574
 
2446
- const VerifyChallengeSuccess = () => {
2447
- const { currentStyles, loginAppTheme } = useAppSettings();
2448
- return /* @__PURE__ */ jsx(
2449
- Wrapper,
2450
- {
2451
- iconId: "logo",
2452
- className: "passflow-verify-challenge-success-wrapper",
2453
- customCss: currentStyles?.custom_css,
2454
- customLogo: currentStyles?.logo_url,
2455
- removeBranding: loginAppTheme?.remove_passflow_logo,
2456
- children: /* @__PURE__ */ jsx("div", { className: "passflow-verify-challenge-success-wrapper", children: /* @__PURE__ */ jsxs("div", { className: "passflow-verify-challenge-success-container", children: [
2457
- /* @__PURE__ */ jsx("p", { className: "passflow-verify-challenge-success-text", children: "Successful verification!" }),
2458
- /* @__PURE__ */ jsx("p", { className: "passflow-verify-challenge-success-text-secondary", children: "But there is no redirect URL for further redirect." })
2459
- ] }) })
2460
- }
2461
- );
2462
- };
2463
-
2464
- const VerifyChallengeOTPRedirect = ({ otp, challengeId, appId }) => {
2465
- const passflow = usePassflow();
2466
- const { navigate } = useNavigation();
2467
- const [paramsError, setParamsError] = useState(null);
2468
- const [showSuccessMessage, setShowSuccessMessage] = useState(false);
2469
- const { fetch, isError, error, isLoading } = usePasswordlessComplete();
2470
- useEffect(() => {
2471
- const fetchData = async () => {
2472
- if (!appId) {
2473
- setParamsError("Missing required param: app_id");
2474
- return;
2475
- }
2476
- if (!otp) {
2477
- setParamsError("Missing required param: otp");
2478
- return;
2479
- }
2480
- if (!challengeId) {
2481
- setParamsError("Missing required param: challenge_id");
2482
- return;
2483
- }
2484
- if (!isLoading) {
2485
- const response = await fetch({ otp, challenge_id: challengeId });
2486
- if (response) {
2487
- if (response.redirect_url) {
2488
- if (!isValidUrl(response.redirect_url)) navigate({ to: response.redirect_url });
2489
- else window.location.href = await getUrlWithTokens(passflow, response.redirect_url);
2490
- } else {
2491
- setShowSuccessMessage(true);
2492
- }
2493
- } else {
2494
- setParamsError("Something went wrong. Please try again later.");
2495
- }
2496
- }
2497
- };
2498
- void fetchData();
2499
- }, []);
2500
- if (isError && error) throw new Error(error);
2501
- if (paramsError) throw new Error(paramsError);
2502
- if (showSuccessMessage) return /* @__PURE__ */ jsx(VerifyChallengeSuccess, {});
2503
- return null;
2504
- };
2505
-
2506
3575
  const TimerButton = ({ totalSecond, onClick, className = "", ...props }) => {
2507
3576
  const [seconds, setSeconds] = useState(totalSecond);
2508
3577
  useEffect(() => {
@@ -2544,6 +3613,24 @@ const TimerButton = ({ totalSecond, onClick, className = "", ...props }) => {
2544
3613
  );
2545
3614
  };
2546
3615
 
3616
+ const VerifyChallengeSuccess = () => {
3617
+ const { currentStyles, loginAppTheme } = useAppSettings();
3618
+ return /* @__PURE__ */ jsx(
3619
+ Wrapper,
3620
+ {
3621
+ iconId: "logo",
3622
+ className: "passflow-verify-challenge-success-wrapper",
3623
+ customCss: currentStyles?.custom_css,
3624
+ customLogo: currentStyles?.logo_url,
3625
+ removeBranding: loginAppTheme?.remove_passflow_logo,
3626
+ children: /* @__PURE__ */ jsx("div", { className: "passflow-verify-challenge-success-wrapper", children: /* @__PURE__ */ jsxs("div", { className: "passflow-verify-challenge-success-container", children: [
3627
+ /* @__PURE__ */ jsx("p", { className: "passflow-verify-challenge-success-text", children: "Successful verification!" }),
3628
+ /* @__PURE__ */ jsx("p", { className: "passflow-verify-challenge-success-text-secondary", children: "But there is no redirect URL for further redirect." })
3629
+ ] }) })
3630
+ }
3631
+ );
3632
+ };
3633
+
2547
3634
  const challengeTypeFullString = {
2548
3635
  email: "email address",
2549
3636
  phone: "phone number"
@@ -2574,8 +3661,7 @@ const VerifyChallengeOTPManual = ({
2574
3661
  const payload = {
2575
3662
  create_tenant: createTenantForNewUser,
2576
3663
  challenge_type: challengeType,
2577
- // biome-ignore lint/style/noNonNullAssertion: <explanation>
2578
- redirect_url: successAuthRedirect ?? appSettings.defaults.redirect,
3664
+ redirect_url: successAuthRedirect ?? appSettings?.defaults.redirect ?? "",
2579
3665
  ...identity === "email" ? { email: identityValue } : { phone: identityValue }
2580
3666
  };
2581
3667
  const refetchResponse = await refetch(payload, type);
@@ -2667,6 +3753,48 @@ const VerifyChallengeOTPManual = ({
2667
3753
  );
2668
3754
  };
2669
3755
 
3756
+ const VerifyChallengeOTPRedirect = ({ otp, challengeId, appId }) => {
3757
+ const passflow = usePassflow();
3758
+ const { navigate } = useNavigation();
3759
+ const [paramsError, setParamsError] = useState(null);
3760
+ const [showSuccessMessage, setShowSuccessMessage] = useState(false);
3761
+ const { fetch, isError, error, isLoading } = usePasswordlessComplete();
3762
+ useEffect(() => {
3763
+ const fetchData = async () => {
3764
+ if (!appId) {
3765
+ setParamsError("Missing required param: app_id");
3766
+ return;
3767
+ }
3768
+ if (!otp) {
3769
+ setParamsError("Missing required param: otp");
3770
+ return;
3771
+ }
3772
+ if (!challengeId) {
3773
+ setParamsError("Missing required param: challenge_id");
3774
+ return;
3775
+ }
3776
+ if (!isLoading) {
3777
+ const response = await fetch({ otp, challenge_id: challengeId });
3778
+ if (response) {
3779
+ if (response.redirect_url) {
3780
+ if (!isValidUrl(response.redirect_url)) navigate({ to: response.redirect_url });
3781
+ else window.location.href = await getUrlWithTokens(passflow, response.redirect_url);
3782
+ } else {
3783
+ setShowSuccessMessage(true);
3784
+ }
3785
+ } else {
3786
+ setParamsError("Something went wrong. Please try again later.");
3787
+ }
3788
+ }
3789
+ };
3790
+ void fetchData();
3791
+ }, []);
3792
+ if (isError && error) throw new Error(error);
3793
+ if (paramsError) throw new Error(paramsError);
3794
+ if (showSuccessMessage) return /* @__PURE__ */ jsx(VerifyChallengeSuccess, {});
3795
+ return null;
3796
+ };
3797
+
2670
3798
  const redirectSearchParamsVerifyChallengeOtpSchema = Yup.object().shape({
2671
3799
  appId: Yup.string().required(),
2672
3800
  challengeId: Yup.string().required(),
@@ -2786,8 +3914,7 @@ const ForgotPassword = ({
2786
3914
  ...isEmail && { email: values.email_or_username },
2787
3915
  ...isUsername && { username: values.email_or_username },
2788
3916
  ...isPhone && { phone: validatedPhone.phoneNumber },
2789
- // biome-ignore lint/style/noNonNullAssertion: <explanation>
2790
- redirect_url: successResetRedirect ?? appSettings.defaults.redirect
3917
+ redirect_url: successResetRedirect ?? appSettings?.defaults.redirect
2791
3918
  };
2792
3919
  const status = await fetch(payload);
2793
3920
  if (status) {
@@ -3005,9 +4132,13 @@ const ResetPassword = ({ successAuthRedirect }) => {
3005
4132
  const resetTokenType = resetTokenData;
3006
4133
  const status = await fetch(values.password);
3007
4134
  if (status) {
3008
- if (!isValidUrl(resetTokenType?.redirect_url ?? successAuthRedirect ?? appSettings.defaults.redirect))
3009
- navigate({ to: resetTokenType?.redirect_url ?? successAuthRedirect ?? appSettings.defaults.redirect });
3010
- else window.location.href = await getUrlWithTokens(passflow, resetTokenType?.redirect_url ?? successAuthRedirect ?? appSettings.defaults.redirect);
4135
+ if (!isValidUrl(resetTokenType?.redirect_url ?? successAuthRedirect ?? appSettings?.defaults.redirect))
4136
+ navigate({ to: resetTokenType?.redirect_url ?? successAuthRedirect ?? appSettings?.defaults.redirect });
4137
+ else
4138
+ window.location.href = await getUrlWithTokens(
4139
+ passflow,
4140
+ resetTokenType?.redirect_url ?? successAuthRedirect ?? appSettings?.defaults.redirect
4141
+ );
3011
4142
  }
3012
4143
  };
3013
4144
  const handlePasswordChange = async () => {
@@ -3140,7 +4271,7 @@ const InvitationJoinFlow = ({
3140
4271
  tenant_name: tenantName,
3141
4272
  redirect_url: redirectUrl
3142
4273
  } = invitationTokenData;
3143
- const parsedTokenCache = passflow.getParsedTokenCache();
4274
+ const parsedTokenCache = passflow.getParsedTokens();
3144
4275
  const onClickNavigateToSignInHandler = () => navigate({ to: signInPath, search: window.location.search });
3145
4276
  const onClickNavigateToSignUpHandler = () => navigate({ to: signUpPath, search: window.location.search });
3146
4277
  if (!parsedTokenCache?.access_token) onClickNavigateToSignInHandler();
@@ -3169,7 +4300,7 @@ const InvitationJoinFlow = ({
3169
4300
  type: "button",
3170
4301
  variant: "primary",
3171
4302
  className: "passflow-button-invitation-join",
3172
- onClick: () => void onClickAcceptInvitationHandler(redirectUrl ?? successAuthRedirect ?? appSettings.defaults.redirect),
4303
+ onClick: () => void onClickAcceptInvitationHandler(redirectUrl ?? successAuthRedirect ?? appSettings?.defaults.redirect),
3173
4304
  disabled: isInvitationJoinLoading,
3174
4305
  children: "Accept invitation"
3175
4306
  }
@@ -4245,6 +5376,13 @@ function useNavigateUnstable() {
4245
5376
  }, [basename, navigator, routePathnamesJson, locationPathname, dataRouterContext]);
4246
5377
  return navigate;
4247
5378
  }
5379
+ function useParams() {
5380
+ let {
5381
+ matches
5382
+ } = React.useContext(RouteContext);
5383
+ let routeMatch = matches[matches.length - 1];
5384
+ return routeMatch ? routeMatch.params : {};
5385
+ }
4248
5386
  function useRoutes(routes, locationArg) {
4249
5387
  return useRoutesImpl(routes, locationArg);
4250
5388
  }
@@ -4498,13 +5636,13 @@ function _renderMatches(matches, parentMatches, dataRouterState, future) {
4498
5636
  }) : getChildren();
4499
5637
  }, null);
4500
5638
  }
4501
- var DataRouterHook$1 = /* @__PURE__ */ function(DataRouterHook2) {
5639
+ var DataRouterHook$1 = /* @__PURE__ */ (function(DataRouterHook2) {
4502
5640
  DataRouterHook2["UseBlocker"] = "useBlocker";
4503
5641
  DataRouterHook2["UseRevalidator"] = "useRevalidator";
4504
5642
  DataRouterHook2["UseNavigateStable"] = "useNavigate";
4505
5643
  return DataRouterHook2;
4506
- }(DataRouterHook$1 || {});
4507
- var DataRouterStateHook$1 = /* @__PURE__ */ function(DataRouterStateHook2) {
5644
+ })(DataRouterHook$1 || {});
5645
+ var DataRouterStateHook$1 = /* @__PURE__ */ (function(DataRouterStateHook2) {
4508
5646
  DataRouterStateHook2["UseBlocker"] = "useBlocker";
4509
5647
  DataRouterStateHook2["UseLoaderData"] = "useLoaderData";
4510
5648
  DataRouterStateHook2["UseActionData"] = "useActionData";
@@ -4516,7 +5654,7 @@ var DataRouterStateHook$1 = /* @__PURE__ */ function(DataRouterStateHook2) {
4516
5654
  DataRouterStateHook2["UseNavigateStable"] = "useNavigate";
4517
5655
  DataRouterStateHook2["UseRouteId"] = "useRouteId";
4518
5656
  return DataRouterStateHook2;
4519
- }(DataRouterStateHook$1 || {});
5657
+ })(DataRouterStateHook$1 || {});
4520
5658
  function useDataRouterContext(hookName) {
4521
5659
  let ctx = React.useContext(DataRouterContext);
4522
5660
  !ctx ? invariant(false) : void 0;
@@ -4541,8 +5679,8 @@ function useCurrentRouteId(hookName) {
4541
5679
  function useRouteError() {
4542
5680
  var _state$errors;
4543
5681
  let error = React.useContext(RouteErrorContext);
4544
- let state = useDataRouterState();
4545
- let routeId = useCurrentRouteId();
5682
+ let state = useDataRouterState(DataRouterStateHook$1.UseRouteError);
5683
+ let routeId = useCurrentRouteId(DataRouterStateHook$1.UseRouteError);
4546
5684
  if (error !== void 0) {
4547
5685
  return error;
4548
5686
  }
@@ -4888,6 +6026,7 @@ const index = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
4888
6026
  useInRouterContext,
4889
6027
  useLocation,
4890
6028
  useNavigate,
6029
+ useParams,
4891
6030
  useRouteError,
4892
6031
  useRoutes
4893
6032
  }, Symbol.toStringTag, { value: 'Module' }));
@@ -4975,7 +6114,8 @@ const PassflowWrapper = ({
4975
6114
  signUpPath: routesWithPrefix.signup,
4976
6115
  forgotPasswordPath: routesWithPrefix.forgot_password,
4977
6116
  verifyMagicLinkPath: routesWithPrefix.verify_magic_link,
4978
- verifyOTPPath: routesWithPrefix.verify_otp
6117
+ verifyOTPPath: routesWithPrefix.verify_otp,
6118
+ twoFactorVerifyPath: routesWithPrefix.two_factor_verify
4979
6119
  }
4980
6120
  )
4981
6121
  }
@@ -5043,6 +6183,34 @@ const PassflowWrapper = ({
5043
6183
  )
5044
6184
  }
5045
6185
  ),
6186
+ /* @__PURE__ */ jsx(
6187
+ Route,
6188
+ {
6189
+ path: routesWithPrefix.two_factor_verify,
6190
+ element: /* @__PURE__ */ jsx(
6191
+ TwoFactorVerifyFlow,
6192
+ {
6193
+ successAuthRedirect,
6194
+ signInPath: routesWithPrefix.signin,
6195
+ twoFactorSetupPath: routesWithPrefix.two_factor_setup
6196
+ }
6197
+ )
6198
+ }
6199
+ ),
6200
+ /* @__PURE__ */ jsx(
6201
+ Route,
6202
+ {
6203
+ path: routesWithPrefix.two_factor_setup,
6204
+ element: /* @__PURE__ */ jsx(
6205
+ TwoFactorSetupFlow,
6206
+ {
6207
+ successAuthRedirect,
6208
+ twoFactorVerifyPath: routesWithPrefix.two_factor_verify,
6209
+ signInPath: routesWithPrefix.signin
6210
+ }
6211
+ )
6212
+ }
6213
+ ),
5046
6214
  /* @__PURE__ */ jsx(
5047
6215
  Route,
5048
6216
  {
@@ -5062,15 +6230,116 @@ const PassflowWrapper = ({
5062
6230
  };
5063
6231
  const PassflowFlow = withError(PassflowWrapper, ErrorComponent);
5064
6232
 
6233
+ const TwoFactorSetupMagicLinkFlow = ({
6234
+ onSuccess,
6235
+ onError,
6236
+ redirectOnSuccess = true,
6237
+ signInPath = routes.signin.path
6238
+ }) => {
6239
+ const { token } = useParams();
6240
+ const { navigate } = useNavigation();
6241
+ const { isLoading, isRetrying, isValidated, error, retry, retryCountdown } = useTwoFactorSetupMagicLink(token || "");
6242
+ useEffect(() => {
6243
+ if (error) {
6244
+ onError?.(error);
6245
+ }
6246
+ }, [error, onError]);
6247
+ const handleSetupComplete = () => {
6248
+ onSuccess?.();
6249
+ if (redirectOnSuccess) {
6250
+ navigate({
6251
+ to: signInPath
6252
+ });
6253
+ }
6254
+ };
6255
+ const handleCancel = () => {
6256
+ navigate({ to: signInPath });
6257
+ };
6258
+ if (isLoading) {
6259
+ return /* @__PURE__ */ jsx(
6260
+ Wrapper,
6261
+ {
6262
+ title: "Validating Magic Link",
6263
+ subtitle: "Please wait while we verify your link...",
6264
+ className: "passflow-2fa-magic-link-loading",
6265
+ children: /* @__PURE__ */ jsxs("div", { className: "passflow-loading-container", role: "status", "aria-live": "polite", "aria-busy": "true", children: [
6266
+ /* @__PURE__ */ jsx(Icon, { id: "logo", type: "general", size: "large", className: "passflow-loading-spinner" }),
6267
+ /* @__PURE__ */ jsx("p", { className: "passflow-loading-text", children: "Validating your magic link..." })
6268
+ ] })
6269
+ }
6270
+ );
6271
+ }
6272
+ if (error) {
6273
+ const isRetryable = error.code === "SERVER_ERROR" || error.code === "RATE_LIMITED";
6274
+ const isRateLimited = error.code === "RATE_LIMITED" && retryCountdown !== null && retryCountdown > 0;
6275
+ return /* @__PURE__ */ jsx(
6276
+ Wrapper,
6277
+ {
6278
+ title: "Magic Link Validation Failed",
6279
+ subtitle: getErrorSubtitle(error.code),
6280
+ className: "passflow-2fa-magic-link-error",
6281
+ children: /* @__PURE__ */ jsxs("div", { className: "passflow-error-container", role: "alert", "aria-live": "assertive", children: [
6282
+ /* @__PURE__ */ jsx("div", { className: "passflow-error-icon", children: /* @__PURE__ */ jsx(Icon, { id: "warning", type: "general", size: "large" }) }),
6283
+ /* @__PURE__ */ jsx("p", { className: "passflow-error-message", id: "error-message", children: error.message }),
6284
+ /* @__PURE__ */ jsxs("div", { className: "passflow-error-actions", children: [
6285
+ isRetryable && /* @__PURE__ */ jsx(
6286
+ Button,
6287
+ {
6288
+ size: "big",
6289
+ type: "button",
6290
+ variant: "primary",
6291
+ onClick: retry,
6292
+ disabled: isRateLimited || isRetrying,
6293
+ "aria-describedby": "error-message",
6294
+ children: isRetrying ? "Retrying..." : isRateLimited ? `Try again in ${formatCountdown(retryCountdown)}` : "Try Again"
6295
+ }
6296
+ ),
6297
+ /* @__PURE__ */ jsx(Button, { size: "big", type: "button", variant: "secondary", onClick: () => navigate({ to: signInPath }), children: "Back to Sign In" })
6298
+ ] }),
6299
+ !isRetryable && /* @__PURE__ */ jsx("p", { className: "passflow-error-help-text", children: "Please contact your administrator to request a new magic link." })
6300
+ ] })
6301
+ }
6302
+ );
6303
+ }
6304
+ if (isValidated) {
6305
+ return /* @__PURE__ */ jsx(TwoFactorSetupForm, { onComplete: handleSetupComplete, onCancel: handleCancel });
6306
+ }
6307
+ return null;
6308
+ };
6309
+ function getErrorSubtitle(code) {
6310
+ switch (code) {
6311
+ case "EXPIRED_TOKEN":
6312
+ return "This magic link has expired";
6313
+ case "INVALID_TOKEN":
6314
+ return "This magic link is invalid";
6315
+ case "REVOKED_TOKEN":
6316
+ return "This magic link has been revoked";
6317
+ case "RATE_LIMITED":
6318
+ return "Too many attempts";
6319
+ case "SERVER_ERROR":
6320
+ return "Something went wrong";
6321
+ default:
6322
+ return "An error occurred";
6323
+ }
6324
+ }
6325
+ function formatCountdown(seconds) {
6326
+ if (seconds === null) return "";
6327
+ const mins = Math.floor(seconds / 60);
6328
+ const secs = seconds % 60;
6329
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
6330
+ }
6331
+
5065
6332
  const PassflowProvider = ({
5066
6333
  children,
5067
6334
  navigate: initialNavigate,
5068
6335
  router = "default",
5069
6336
  ...config
5070
6337
  }) => {
6338
+ const needsDiscovery = !config.appId;
5071
6339
  const [state, dispatch] = useReducer(passflowReducer, {
5072
6340
  ...initialState,
5073
- ...config
6341
+ ...config,
6342
+ isDiscoveringAppId: needsDiscovery
5074
6343
  });
5075
6344
  const [navigate, setNavigate] = useState(() => {
5076
6345
  if (initialNavigate) {
@@ -5079,6 +6348,51 @@ const PassflowProvider = ({
5079
6348
  return defaultNavigate;
5080
6349
  });
5081
6350
  const passflow = useMemo(() => new Passflow(state), [state]);
6351
+ const discoveryAttemptedRef = useRef(false);
6352
+ useEffect(() => {
6353
+ if (needsDiscovery && !discoveryAttemptedRef.current && state.isDiscoveringAppId) {
6354
+ discoveryAttemptedRef.current = true;
6355
+ const discoverAppId = async () => {
6356
+ const urlsToTry = ["/settings"];
6357
+ if (config.url) {
6358
+ urlsToTry.push(`${config.url}/settings`);
6359
+ }
6360
+ for (const url of urlsToTry) {
6361
+ try {
6362
+ const response = await fetch(url);
6363
+ if (response.ok) {
6364
+ const settings = await response.json();
6365
+ const discoveredAppId = settings.login_app?.app_id || settings.appId;
6366
+ if (discoveredAppId) {
6367
+ passflow.setAppId(discoveredAppId);
6368
+ dispatch({
6369
+ type: "SET_PASSFLOW_STATE",
6370
+ payload: {
6371
+ ...state,
6372
+ appId: discoveredAppId,
6373
+ scopes: settings.login_app?.scopes || settings.scopes || state.scopes,
6374
+ createTenantForNewUser: settings.login_app?.create_tenant_for_new_user ?? settings.createTenantForNewUser ?? state.createTenantForNewUser,
6375
+ isDiscoveringAppId: false
6376
+ }
6377
+ });
6378
+ return;
6379
+ }
6380
+ }
6381
+ } catch (error) {
6382
+ }
6383
+ }
6384
+ console.warn("Failed to discover appId from /settings");
6385
+ dispatch({
6386
+ type: "SET_PASSFLOW_STATE",
6387
+ payload: {
6388
+ ...state,
6389
+ isDiscoveringAppId: false
6390
+ }
6391
+ });
6392
+ };
6393
+ void discoverAppId();
6394
+ }
6395
+ }, [needsDiscovery, state.isDiscoveringAppId, config.url, passflow, state]);
5082
6396
  const passflowValue = useMemo(() => ({ state, dispatch, passflow }), [state, passflow]);
5083
6397
  const handleSetNavigate = useCallback((newNavigate) => {
5084
6398
  setNavigate(() => newNavigate || defaultNavigate);
@@ -5094,5 +6408,5 @@ const PassflowProvider = ({
5094
6408
  return /* @__PURE__ */ jsx(PassflowContext.Provider, { value: passflowValue, children: /* @__PURE__ */ jsx(NavigationContext$1.Provider, { value: navigationValue, children: /* @__PURE__ */ jsx(AuthProvider, { children }) }) });
5095
6409
  };
5096
6410
 
5097
- export { Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, FieldPassword, FieldPhone, FieldText, ForgotPassword, ForgotPasswordSuccess, Icon, InvitationJoin, Link, PassflowFlow, PassflowProvider, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, ProvidersBox, ResetPassword, SignIn, SignInForm, SignUp, SignUpForm, Switch, VerifyChallengeMagicLink, VerifyChallengeOTP, Wrapper, useAppSettings, useAuth, useAuthCloudRedirect, useForgotPassword, useJoinInvite, useLogout, useNavigation, useOutsideClick, usePassflow, usePasswordlessComplete, useProvider, useResetPassword, useSignIn, useSignUp, useUserPasskeys };
6411
+ export { Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, FieldPassword, FieldPhone, FieldText, ForgotPassword, ForgotPasswordSuccess, Icon, InvitationJoin, Link, PassflowFlow, PassflowProvider, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, ProvidersBox, ResetPassword, SignIn, SignInForm, SignUp, SignUpForm, Switch, TwoFactorRecoveryForm, TwoFactorSetupForm, TwoFactorSetupMagicLinkFlow, TwoFactorVerifyForm, VerifyChallengeMagicLink, VerifyChallengeOTP, Wrapper, useAppSettings, useAuth, useAuthCloudRedirect, useForgotPassword, useJoinInvite, useLogout, useNavigation, useOutsideClick, usePassflow, usePasswordlessComplete, useProvider, useResetPassword, useSignIn, useSignUp, useTwoFactorManage, useTwoFactorSetup, useTwoFactorSetupMagicLink, useTwoFactorStatus, useTwoFactorVerify, useUserPasskeys };
5098
6412
  //# sourceMappingURL=index.es.js.map