@passflow/react 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) 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 +1440 -119
  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/index.d.ts +2 -0
  16. package/dist/src/components/form/index.d.ts.map +1 -1
  17. package/dist/src/components/form/invitation-join/index.d.ts +1 -1
  18. package/dist/src/components/form/invitation-join/index.d.ts.map +1 -1
  19. package/dist/src/components/form/reset-password/index.d.ts.map +1 -1
  20. package/dist/src/components/form/signin/index.d.ts +2 -1
  21. package/dist/src/components/form/signin/index.d.ts.map +1 -1
  22. package/dist/src/components/form/signup/index.d.ts +1 -1
  23. package/dist/src/components/form/signup/index.d.ts.map +1 -1
  24. package/dist/src/components/form/two-factor-setup/index.d.ts +2 -0
  25. package/dist/src/components/form/two-factor-setup/index.d.ts.map +1 -0
  26. package/dist/src/components/form/two-factor-setup/two-factor-setup-form.d.ts +9 -0
  27. package/dist/src/components/form/two-factor-setup/two-factor-setup-form.d.ts.map +1 -0
  28. package/dist/src/components/form/two-factor-verify/index.d.ts +3 -0
  29. package/dist/src/components/form/two-factor-verify/index.d.ts.map +1 -0
  30. package/dist/src/components/form/two-factor-verify/two-factor-recovery-form.d.ts +11 -0
  31. package/dist/src/components/form/two-factor-verify/two-factor-recovery-form.d.ts.map +1 -0
  32. package/dist/src/components/form/two-factor-verify/two-factor-verify-form.d.ts +14 -0
  33. package/dist/src/components/form/two-factor-verify/two-factor-verify-form.d.ts.map +1 -0
  34. package/dist/src/components/form/verify-challenge/{varify-challenge-otp-redirect.d.ts → verify-challenge-otp-redirect.d.ts} +1 -1
  35. package/dist/src/components/form/verify-challenge/{varify-challenge-otp-redirect.d.ts.map → verify-challenge-otp-redirect.d.ts.map} +1 -1
  36. package/dist/src/components/form/verify-challenge/{varify-challenge-success.d.ts → verify-challenge-success.d.ts} +1 -1
  37. package/dist/src/components/form/verify-challenge/verify-challenge-success.d.ts.map +1 -0
  38. package/dist/src/components/provider/passflow-provider.d.ts.map +1 -1
  39. package/dist/src/context/passflow-context.d.ts +2 -0
  40. package/dist/src/context/passflow-context.d.ts.map +1 -1
  41. package/dist/src/context/router-context.d.ts +12 -0
  42. package/dist/src/context/router-context.d.ts.map +1 -1
  43. package/dist/src/hocs/with-error.d.ts +1 -1
  44. package/dist/src/hocs/with-error.d.ts.map +1 -1
  45. package/dist/src/hooks/index.d.ts +5 -0
  46. package/dist/src/hooks/index.d.ts.map +1 -1
  47. package/dist/src/hooks/use-app-settings.d.ts.map +1 -1
  48. package/dist/src/hooks/use-passflow-store.d.ts.map +1 -1
  49. package/dist/src/hooks/use-signin.d.ts.map +1 -1
  50. package/dist/src/hooks/use-two-factor-manage.d.ts +15 -0
  51. package/dist/src/hooks/use-two-factor-manage.d.ts.map +1 -0
  52. package/dist/src/hooks/use-two-factor-setup-magic-link.d.ts +55 -0
  53. package/dist/src/hooks/use-two-factor-setup-magic-link.d.ts.map +1 -0
  54. package/dist/src/hooks/use-two-factor-setup.d.ts +18 -0
  55. package/dist/src/hooks/use-two-factor-setup.d.ts.map +1 -0
  56. package/dist/src/hooks/use-two-factor-status.d.ts +13 -0
  57. package/dist/src/hooks/use-two-factor-status.d.ts.map +1 -0
  58. package/dist/src/hooks/use-two-factor-verify.d.ts +18 -0
  59. package/dist/src/hooks/use-two-factor-verify.d.ts.map +1 -0
  60. package/dist/src/test/setup.d.ts +1 -0
  61. package/dist/src/test/setup.d.ts.map +1 -0
  62. package/dist/src/test/utils/render.d.ts +4 -0
  63. package/dist/src/test/utils/render.d.ts.map +1 -0
  64. package/dist/src/test/utils/test-passflow.d.ts +6 -0
  65. package/dist/src/test/utils/test-passflow.d.ts.map +1 -0
  66. package/dist/src/types/index.d.ts +1 -0
  67. package/dist/src/types/index.d.ts.map +1 -1
  68. package/dist/src/types/two-factor-errors.d.ts +14 -0
  69. package/dist/src/types/two-factor-errors.d.ts.map +1 -0
  70. package/dist/src/utils/classify-two-factor-error.d.ts +13 -0
  71. package/dist/src/utils/classify-two-factor-error.d.ts.map +1 -0
  72. package/dist/src/utils/{cn/index.d.ts → cn.d.ts} +1 -1
  73. package/dist/src/utils/cn.d.ts.map +1 -0
  74. package/dist/src/utils/{get-app-version/index.d.ts → get-app-version.d.ts} +1 -1
  75. package/dist/src/utils/get-app-version.d.ts.map +1 -0
  76. package/dist/src/utils/{get-auth-methods/index.d.ts → get-auth-methods.d.ts} +1 -1
  77. package/dist/src/utils/get-auth-methods.d.ts.map +1 -0
  78. package/dist/src/utils/{get-form-labels/index.d.ts → get-form-labels.d.ts} +3 -3
  79. package/dist/src/utils/get-form-labels.d.ts.map +1 -0
  80. package/dist/src/utils/{get-url-errors/index.d.ts → get-url-errors.d.ts} +1 -1
  81. package/dist/src/utils/get-url-errors.d.ts.map +1 -0
  82. package/dist/src/utils/get-url-with-tokens.d.ts +4 -0
  83. package/dist/src/utils/get-url-with-tokens.d.ts.map +1 -0
  84. package/dist/src/utils/index.d.ts +3 -1
  85. package/dist/src/utils/index.d.ts.map +1 -1
  86. package/dist/src/utils/two-factor-loop-prevention.d.ts +40 -0
  87. package/dist/src/utils/two-factor-loop-prevention.d.ts.map +1 -0
  88. package/dist/src/utils/{undefined-on-catch/index.d.ts → undefined-on-catch.d.ts} +1 -1
  89. package/dist/src/utils/undefined-on-catch.d.ts.map +1 -0
  90. package/dist/src/utils/{url-params/index.d.ts → url-params.d.ts} +1 -1
  91. package/dist/src/utils/url-params.d.ts.map +1 -0
  92. package/dist/src/utils/{validate-url/index.d.ts → validate-url.d.ts} +1 -1
  93. package/dist/src/utils/validate-url.d.ts.map +1 -0
  94. package/dist/style.css +1 -1
  95. package/package.json +31 -31
  96. package/dist/src/components/form/verify-challenge/varify-challenge-success.d.ts.map +0 -1
  97. package/dist/src/utils/cn/index.d.ts.map +0 -1
  98. package/dist/src/utils/get-app-version/index.d.ts.map +0 -1
  99. package/dist/src/utils/get-auth-methods/index.d.ts.map +0 -1
  100. package/dist/src/utils/get-form-labels/index.d.ts.map +0 -1
  101. package/dist/src/utils/get-url-errors/index.d.ts.map +0 -1
  102. package/dist/src/utils/get-url-with-tokens/index.d.ts +0 -3
  103. package/dist/src/utils/get-url-with-tokens/index.d.ts.map +0 -1
  104. package/dist/src/utils/undefined-on-catch/index.d.ts.map +0 -1
  105. package/dist/src/utils/url-params/index.d.ts.map +0 -1
  106. 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.1.42";
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,140 @@ 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
+ case "generic":
296
+ default:
297
+ return error.message;
298
+ }
299
+ }
300
+
301
+ const STORAGE_KEY_PREFIX = "passflow_2fa";
302
+ const REDIRECT_COUNT_KEY = `${STORAGE_KEY_PREFIX}_redirect_count`;
303
+ const LAST_ERROR_KEY = `${STORAGE_KEY_PREFIX}_last_error_type`;
304
+ const MAX_REDIRECTS = 3;
305
+ const TwoFactorLoopPrevention = {
306
+ /**
307
+ * Check if redirect should be allowed
308
+ * @returns true if redirect is safe, false if loop detected
309
+ */
310
+ canRedirect() {
311
+ try {
312
+ const count = this.getRedirectCount();
313
+ return count < MAX_REDIRECTS;
314
+ } catch {
315
+ return true;
316
+ }
317
+ },
318
+ /**
319
+ * Increment redirect counter
320
+ */
321
+ incrementRedirect() {
322
+ try {
323
+ const count = this.getRedirectCount();
324
+ sessionStorage.setItem(REDIRECT_COUNT_KEY, String(count + 1));
325
+ } catch {
326
+ }
327
+ },
328
+ /**
329
+ * Get current redirect count
330
+ */
331
+ getRedirectCount() {
332
+ try {
333
+ const value = sessionStorage.getItem(REDIRECT_COUNT_KEY);
334
+ return value ? Number.parseInt(value, 10) : 0;
335
+ } catch {
336
+ return 0;
337
+ }
338
+ },
339
+ /**
340
+ * Reset redirect counter (call on successful auth)
341
+ */
342
+ reset() {
343
+ try {
344
+ sessionStorage.removeItem(REDIRECT_COUNT_KEY);
345
+ sessionStorage.removeItem(LAST_ERROR_KEY);
346
+ } catch {
347
+ }
348
+ },
349
+ /**
350
+ * Track last error type to detect repeated errors
351
+ */
352
+ setLastErrorType(errorType) {
353
+ try {
354
+ sessionStorage.setItem(LAST_ERROR_KEY, errorType);
355
+ } catch {
356
+ }
357
+ },
358
+ /**
359
+ * Get last error type
360
+ */
361
+ getLastErrorType() {
362
+ try {
363
+ return sessionStorage.getItem(LAST_ERROR_KEY);
364
+ } catch {
365
+ return null;
366
+ }
367
+ },
368
+ /**
369
+ * Check if same error is repeating
370
+ */
371
+ isRepeatingError(errorType) {
372
+ const lastError = this.getLastErrorType();
373
+ return lastError === errorType && this.getRedirectCount() > 0;
374
+ },
375
+ /**
376
+ * Get user-friendly message when loop is detected
377
+ */
378
+ getLoopDetectedMessage() {
379
+ return "Unable to complete authentication. This may be a configuration issue. Please contact support.";
380
+ }
381
+ };
382
+
246
383
  const flags = "data:image/png;base64,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";
247
384
 
248
385
  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 +717,9 @@ const initialState = {
580
717
  appId: void 0,
581
718
  scopes: void 0,
582
719
  createTenantForNewUser: void 0,
583
- parseQueryParams: true
720
+ parseQueryParams: true,
721
+ isDiscoveringAppId: false,
722
+ hasSettingsError: false
584
723
  };
585
724
  const PassflowContext = createContext(void 0);
586
725
  const passflowReducer = (state, action) => {
@@ -619,6 +758,18 @@ const routes = {
619
758
  },
620
759
  passkey: {
621
760
  path: "/passkeysss"
761
+ },
762
+ two_factor_verify: {
763
+ path: "/two-factor-verify"
764
+ },
765
+ two_factor_recovery: {
766
+ path: "/two-factor-recovery"
767
+ },
768
+ two_factor_setup: {
769
+ path: "/two-factor-setup"
770
+ },
771
+ two_factor_setup_magic_link: {
772
+ path: "/two-factor-setup-magic-link/:token"
622
773
  }
623
774
  };
624
775
 
@@ -646,7 +797,7 @@ const AuthProvider = ({ children }) => {
646
797
  setIsLoading(true);
647
798
  try {
648
799
  const tokens = await passflow.getTokens(doRefresh);
649
- const parsedTokens = tokens ? passflow.getParsedTokenCache() : void 0;
800
+ const parsedTokens = tokens ? passflow.getParsedTokens() : void 0;
650
801
  return {
651
802
  tokens,
652
803
  parsedTokens
@@ -697,17 +848,13 @@ const useSignIn = () => {
697
848
  else if (type === "passkey") {
698
849
  await passflow.passkeyAuthenticate(payload);
699
850
  } else {
700
- const passwordlessResponse = await passflow.passwordlessSignIn(payload);
701
- cleanup();
702
- return passwordlessResponse;
851
+ return await passflow.passwordlessSignIn(payload);
703
852
  }
704
- cleanup();
705
853
  return true;
706
854
  } catch (e) {
707
855
  setIsError(true);
708
856
  const error = e;
709
857
  setErrorMessage(error.message);
710
- cleanup();
711
858
  return false;
712
859
  } finally {
713
860
  cleanup();
@@ -852,48 +999,143 @@ const useAppSettings = () => {
852
999
  const [isError, setIsError] = useState(false);
853
1000
  const [errorMessage, setErrorMessage] = useState("");
854
1001
  const [isLoading, setIsLoading] = useState(false);
1002
+ const isFetchingRef = useRef(false);
855
1003
  if (!context) {
856
1004
  throw new Error("useAppSetting must be used within an PassflowProvider");
857
1005
  }
858
1006
  const { state, dispatch } = context;
859
1007
  useLayoutEffect(() => {
860
- if (!state.appSettings) {
1008
+ if (!state.appSettings && !state.hasSettingsError && !isFetchingRef.current) {
861
1009
  const fetchAllSettings = async () => {
1010
+ isFetchingRef.current = true;
862
1011
  setIsLoading(true);
863
1012
  try {
1013
+ if (!passflow.appId && !state.isDiscoveringAppId) {
1014
+ dispatch({
1015
+ type: "SET_PASSFLOW_STATE",
1016
+ payload: {
1017
+ ...state,
1018
+ isDiscoveringAppId: true
1019
+ }
1020
+ });
1021
+ try {
1022
+ const urlsToTry = ["/settings"];
1023
+ if (passflow.url) {
1024
+ urlsToTry.push(`${passflow.url}/settings`);
1025
+ }
1026
+ let response = null;
1027
+ for (const url of urlsToTry) {
1028
+ try {
1029
+ const r = await fetch(url);
1030
+ if (r.ok) {
1031
+ response = r;
1032
+ break;
1033
+ }
1034
+ } catch {
1035
+ continue;
1036
+ }
1037
+ }
1038
+ if (response && response.ok) {
1039
+ const settings = await response.json();
1040
+ const discoveredAppId = settings.login_app?.app_id || settings.appId;
1041
+ if (discoveredAppId) {
1042
+ passflow.setAppId(discoveredAppId);
1043
+ const stateUpdates = {
1044
+ appId: discoveredAppId
1045
+ };
1046
+ if (!state.scopes) {
1047
+ const discoveredScopes = settings.login_app?.scopes || settings.scopes;
1048
+ if (discoveredScopes) {
1049
+ stateUpdates.scopes = discoveredScopes;
1050
+ }
1051
+ }
1052
+ if (isUndefined(state.createTenantForNewUser)) {
1053
+ const createTenant = settings.login_app?.create_tenant_for_new_user ?? settings.createTenantForNewUser;
1054
+ if (createTenant !== void 0) {
1055
+ stateUpdates.createTenantForNewUser = createTenant;
1056
+ }
1057
+ }
1058
+ dispatch({
1059
+ type: "SET_PASSFLOW_STATE",
1060
+ payload: {
1061
+ ...state,
1062
+ ...stateUpdates,
1063
+ isDiscoveringAppId: false
1064
+ }
1065
+ });
1066
+ } else {
1067
+ dispatch({
1068
+ type: "SET_PASSFLOW_STATE",
1069
+ payload: {
1070
+ ...state,
1071
+ isDiscoveringAppId: false
1072
+ }
1073
+ });
1074
+ }
1075
+ }
1076
+ } catch (discoveryError) {
1077
+ console.warn("Failed to discover appId from /settings:", discoveryError);
1078
+ dispatch({
1079
+ type: "SET_PASSFLOW_STATE",
1080
+ payload: {
1081
+ ...state,
1082
+ isDiscoveringAppId: false
1083
+ }
1084
+ });
1085
+ }
1086
+ }
864
1087
  let appSettings = {};
865
1088
  if (passflow.appId) {
866
1089
  appSettings = await passflow.getAppSettings();
867
1090
  }
868
- if (!state.scopes) state.scopes = appSettings.defaults.scopes;
869
- if (isUndefined(state.createTenantForNewUser)) state.createTenantForNewUser = appSettings.defaults.create_tenant_for_new_user;
1091
+ const finalStateUpdates = {
1092
+ appSettings
1093
+ };
1094
+ if (!state.scopes) finalStateUpdates.scopes = appSettings.defaults?.scopes;
1095
+ if (isUndefined(state.createTenantForNewUser))
1096
+ finalStateUpdates.createTenantForNewUser = appSettings.defaults?.create_tenant_for_new_user;
870
1097
  let passwordPolicy = null;
871
1098
  if (appSettings.auth_strategies && hasPasswordStrategy(appSettings.auth_strategies)) {
872
1099
  passwordPolicy = await passflow.getPasswordPolicySettings();
873
1100
  }
1101
+ finalStateUpdates.passwordPolicy = passwordPolicy;
874
1102
  dispatch({
875
1103
  type: "SET_PASSFLOW_STATE",
876
1104
  payload: {
877
1105
  ...state,
878
- appSettings,
879
- passwordPolicy
1106
+ ...finalStateUpdates
880
1107
  }
881
1108
  });
882
1109
  } catch (e) {
883
1110
  setIsError(true);
884
1111
  const error = e;
885
1112
  setErrorMessage(error.message);
1113
+ dispatch({
1114
+ type: "SET_PASSFLOW_STATE",
1115
+ payload: {
1116
+ ...state,
1117
+ hasSettingsError: true
1118
+ }
1119
+ });
886
1120
  } finally {
887
1121
  setIsLoading(false);
1122
+ isFetchingRef.current = false;
888
1123
  }
889
1124
  };
890
1125
  void fetchAllSettings();
891
1126
  }
892
- }, [dispatch, state.appSettings, passflow, state]);
1127
+ }, [dispatch, state.appSettings, state.hasSettingsError, state.isDiscoveringAppId, passflow]);
893
1128
  const reset = () => {
894
1129
  setIsError(false);
895
1130
  setErrorMessage("");
896
1131
  setIsLoading(false);
1132
+ dispatch({
1133
+ type: "SET_PASSFLOW_STATE",
1134
+ payload: {
1135
+ ...state,
1136
+ hasSettingsError: false
1137
+ }
1138
+ });
897
1139
  };
898
1140
  const applyThemeStyles = (style) => {
899
1141
  const root = document.documentElement;
@@ -1146,7 +1388,7 @@ const useNavigation = () => {
1146
1388
  setNavigate(null);
1147
1389
  return;
1148
1390
  }
1149
- const wrappedNavigate = (options) => {
1391
+ const wrappedNavigate = ((options) => {
1150
1392
  const { to, search = "", replace = false } = options;
1151
1393
  switch (router) {
1152
1394
  case "react-router":
@@ -1177,7 +1419,7 @@ const useNavigation = () => {
1177
1419
  }
1178
1420
  }
1179
1421
  }
1180
- };
1422
+ });
1181
1423
  setNavigate(wrappedNavigate);
1182
1424
  },
1183
1425
  [setNavigate, router]
@@ -1189,6 +1431,364 @@ const useNavigation = () => {
1189
1431
  };
1190
1432
  };
1191
1433
 
1434
+ const useTwoFactorStatus = () => {
1435
+ const passflow = usePassflow();
1436
+ const [data, setData] = useState(null);
1437
+ const [isLoading, setIsLoading] = useState(false);
1438
+ const [isError, setIsError] = useState(false);
1439
+ const [errorMessage, setErrorMessage] = useState("");
1440
+ const fetch = useCallback(async () => {
1441
+ setIsLoading(true);
1442
+ setIsError(false);
1443
+ setErrorMessage("");
1444
+ try {
1445
+ const response = await passflow.getTwoFactorStatus();
1446
+ setData(response);
1447
+ return response;
1448
+ } catch (e) {
1449
+ setIsError(true);
1450
+ const error = e;
1451
+ setErrorMessage(error.message);
1452
+ return null;
1453
+ } finally {
1454
+ setIsLoading(false);
1455
+ }
1456
+ }, [passflow]);
1457
+ useLayoutEffect(() => {
1458
+ void fetch();
1459
+ }, [fetch]);
1460
+ return {
1461
+ data,
1462
+ refetch: fetch,
1463
+ isLoading,
1464
+ isError,
1465
+ errorMessage
1466
+ };
1467
+ };
1468
+
1469
+ const useTwoFactorSetup = () => {
1470
+ const passflow = usePassflow();
1471
+ const [setupData, setSetupData] = useState(null);
1472
+ const [recoveryCodes, setRecoveryCodes] = useState([]);
1473
+ const [isLoading, setIsLoading] = useState(false);
1474
+ const [isError, setIsError] = useState(false);
1475
+ const [error, setError] = useState("");
1476
+ const [step, setStep] = useState("idle");
1477
+ const beginSetup = useCallback(async () => {
1478
+ setIsLoading(true);
1479
+ setIsError(false);
1480
+ setError("");
1481
+ try {
1482
+ const response = await passflow.beginTwoFactorSetup();
1483
+ setSetupData(response);
1484
+ setStep("setup");
1485
+ return response;
1486
+ } catch (e) {
1487
+ setIsError(true);
1488
+ const err = e;
1489
+ setError(err.message);
1490
+ return null;
1491
+ } finally {
1492
+ setIsLoading(false);
1493
+ }
1494
+ }, [passflow]);
1495
+ const confirmSetup = useCallback(
1496
+ async (code) => {
1497
+ setIsLoading(true);
1498
+ setIsError(false);
1499
+ setError("");
1500
+ try {
1501
+ const response = await passflow.confirmTwoFactorSetup(code);
1502
+ setRecoveryCodes(response.recovery_codes);
1503
+ setStep("complete");
1504
+ return response;
1505
+ } catch (e) {
1506
+ setIsError(true);
1507
+ const err = e;
1508
+ setError(err.message);
1509
+ return null;
1510
+ } finally {
1511
+ setIsLoading(false);
1512
+ }
1513
+ },
1514
+ [passflow]
1515
+ );
1516
+ const reset = useCallback(() => {
1517
+ setSetupData(null);
1518
+ setRecoveryCodes([]);
1519
+ setIsError(false);
1520
+ setError("");
1521
+ setStep("idle");
1522
+ }, []);
1523
+ return {
1524
+ setupData,
1525
+ recoveryCodes,
1526
+ step,
1527
+ beginSetup,
1528
+ confirmSetup,
1529
+ reset,
1530
+ isLoading,
1531
+ isError,
1532
+ error
1533
+ };
1534
+ };
1535
+
1536
+ const useTwoFactorVerify = () => {
1537
+ const passflow = usePassflow();
1538
+ const [isLoading, setIsLoading] = useState(false);
1539
+ const [isError, setIsError] = useState(false);
1540
+ const [error, setError] = useState("");
1541
+ const [errorType, setErrorType] = useState(null);
1542
+ const [errorDetails, setErrorDetails] = useState(null);
1543
+ const isVerificationRequired = useCallback(() => {
1544
+ return passflow.isTwoFactorVerificationRequired();
1545
+ }, [passflow]);
1546
+ const handleError = useCallback((e) => {
1547
+ const err = e;
1548
+ const classified = classifyTwoFactorError(err.message);
1549
+ setIsError(true);
1550
+ setError(err.message);
1551
+ setErrorType(classified.type);
1552
+ setErrorDetails(classified);
1553
+ }, []);
1554
+ const verify = useCallback(
1555
+ async (code) => {
1556
+ setIsLoading(true);
1557
+ setIsError(false);
1558
+ setError("");
1559
+ setErrorType(null);
1560
+ setErrorDetails(null);
1561
+ try {
1562
+ const response = await passflow.verifyTwoFactor(code);
1563
+ return response;
1564
+ } catch (e) {
1565
+ handleError(e);
1566
+ return null;
1567
+ } finally {
1568
+ setIsLoading(false);
1569
+ }
1570
+ },
1571
+ [passflow, handleError]
1572
+ );
1573
+ const useRecoveryCode = useCallback(
1574
+ async (code) => {
1575
+ setIsLoading(true);
1576
+ setIsError(false);
1577
+ setError("");
1578
+ setErrorType(null);
1579
+ setErrorDetails(null);
1580
+ try {
1581
+ const response = await passflow.useTwoFactorRecoveryCode(code);
1582
+ return response;
1583
+ } catch (e) {
1584
+ handleError(e);
1585
+ return null;
1586
+ } finally {
1587
+ setIsLoading(false);
1588
+ }
1589
+ },
1590
+ [passflow, handleError]
1591
+ );
1592
+ const reset = useCallback(() => {
1593
+ setIsError(false);
1594
+ setError("");
1595
+ setErrorType(null);
1596
+ setErrorDetails(null);
1597
+ }, []);
1598
+ return {
1599
+ isVerificationRequired,
1600
+ verify,
1601
+ useRecoveryCode,
1602
+ reset,
1603
+ isLoading,
1604
+ isError,
1605
+ error,
1606
+ errorType,
1607
+ errorDetails
1608
+ };
1609
+ };
1610
+
1611
+ const useTwoFactorManage = () => {
1612
+ const passflow = usePassflow();
1613
+ const [recoveryCodes, setRecoveryCodes] = useState([]);
1614
+ const [isLoading, setIsLoading] = useState(false);
1615
+ const [isError, setIsError] = useState(false);
1616
+ const [error, setError] = useState("");
1617
+ const disable = useCallback(
1618
+ async (code) => {
1619
+ setIsLoading(true);
1620
+ setIsError(false);
1621
+ setError("");
1622
+ try {
1623
+ await passflow.disableTwoFactor(code);
1624
+ return true;
1625
+ } catch (e) {
1626
+ setIsError(true);
1627
+ const err = e;
1628
+ setError(err.message);
1629
+ return false;
1630
+ } finally {
1631
+ setIsLoading(false);
1632
+ }
1633
+ },
1634
+ [passflow]
1635
+ );
1636
+ const regenerateCodes = useCallback(
1637
+ async (code) => {
1638
+ setIsLoading(true);
1639
+ setIsError(false);
1640
+ setError("");
1641
+ try {
1642
+ const response = await passflow.regenerateTwoFactorRecoveryCodes(code);
1643
+ setRecoveryCodes(response.recovery_codes);
1644
+ return response;
1645
+ } catch (e) {
1646
+ setIsError(true);
1647
+ const err = e;
1648
+ setError(err.message);
1649
+ return null;
1650
+ } finally {
1651
+ setIsLoading(false);
1652
+ }
1653
+ },
1654
+ [passflow]
1655
+ );
1656
+ const reset = useCallback(() => {
1657
+ setRecoveryCodes([]);
1658
+ setIsError(false);
1659
+ setError("");
1660
+ }, []);
1661
+ return {
1662
+ recoveryCodes,
1663
+ disable,
1664
+ regenerateCodes,
1665
+ reset,
1666
+ isLoading,
1667
+ isError,
1668
+ error
1669
+ };
1670
+ };
1671
+
1672
+ const useTwoFactorSetupMagicLink = (token) => {
1673
+ const passflow = usePassflow();
1674
+ const [isLoading, setIsLoading] = useState(true);
1675
+ const [isRetrying, setIsRetrying] = useState(false);
1676
+ const [isValidated, setIsValidated] = useState(false);
1677
+ const [error, setError] = useState(null);
1678
+ const [sessionToken, setSessionToken] = useState(null);
1679
+ const [userId, setUserId] = useState(null);
1680
+ const [appId, setAppId] = useState(null);
1681
+ const [expiresIn, setExpiresIn] = useState(null);
1682
+ const [retryCountdown, setRetryCountdown] = useState(null);
1683
+ const [validatedToken, setValidatedToken] = useState(null);
1684
+ const shouldClearOnUnmount = useRef(true);
1685
+ const countdownTimerRef = useRef(null);
1686
+ const startCountdown = useCallback((seconds) => {
1687
+ if (countdownTimerRef.current) {
1688
+ clearInterval(countdownTimerRef.current);
1689
+ }
1690
+ setRetryCountdown(seconds);
1691
+ countdownTimerRef.current = setInterval(() => {
1692
+ setRetryCountdown((prev) => {
1693
+ if (prev === null || prev <= 1) {
1694
+ if (countdownTimerRef.current) {
1695
+ clearInterval(countdownTimerRef.current);
1696
+ countdownTimerRef.current = null;
1697
+ }
1698
+ return null;
1699
+ }
1700
+ return prev - 1;
1701
+ });
1702
+ }, 1e3);
1703
+ }, []);
1704
+ const validateToken = useCallback(
1705
+ async (isRetry = false) => {
1706
+ if (!isRetry && validatedToken === token && isValidated) {
1707
+ return;
1708
+ }
1709
+ if (isRetry) {
1710
+ setIsRetrying(true);
1711
+ } else {
1712
+ setIsLoading(true);
1713
+ }
1714
+ setError(null);
1715
+ try {
1716
+ const response = await passflow.validateTwoFactorSetupMagicLink(token);
1717
+ if (response.success && response.sessionToken && response.userId) {
1718
+ setSessionToken(response.sessionToken);
1719
+ setUserId(response.userId);
1720
+ setAppId(response.appId ?? null);
1721
+ setExpiresIn(response.expiresIn ?? null);
1722
+ setIsValidated(true);
1723
+ setValidatedToken(token);
1724
+ setRetryCountdown(null);
1725
+ shouldClearOnUnmount.current = false;
1726
+ } else if (response.error) {
1727
+ setError(response.error);
1728
+ setIsValidated(false);
1729
+ shouldClearOnUnmount.current = true;
1730
+ if (response.error.code === "RATE_LIMITED" && response.error.retryAfter) {
1731
+ startCountdown(response.error.retryAfter);
1732
+ }
1733
+ }
1734
+ } catch (err) {
1735
+ setError({
1736
+ code: "SERVER_ERROR",
1737
+ message: err instanceof Error ? err.message : "An unexpected error occurred"
1738
+ });
1739
+ setIsValidated(false);
1740
+ shouldClearOnUnmount.current = true;
1741
+ } finally {
1742
+ setIsLoading(false);
1743
+ setIsRetrying(false);
1744
+ }
1745
+ },
1746
+ [token, validatedToken, isValidated, passflow, startCountdown]
1747
+ );
1748
+ const retry = useCallback(async () => {
1749
+ if (retryCountdown !== null && retryCountdown > 0) {
1750
+ return;
1751
+ }
1752
+ setValidatedToken(null);
1753
+ await validateToken(true);
1754
+ }, [validateToken, retryCountdown]);
1755
+ useEffect(() => {
1756
+ if (!token) {
1757
+ setError({
1758
+ code: "INVALID_TOKEN",
1759
+ message: "No token provided"
1760
+ });
1761
+ setIsLoading(false);
1762
+ return;
1763
+ }
1764
+ if (token !== validatedToken) {
1765
+ validateToken();
1766
+ }
1767
+ }, [token, validatedToken, validateToken]);
1768
+ useEffect(() => {
1769
+ return () => {
1770
+ if (countdownTimerRef.current) {
1771
+ clearInterval(countdownTimerRef.current);
1772
+ }
1773
+ if (shouldClearOnUnmount.current) {
1774
+ passflow.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,515 @@ 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, index) => /* @__PURE__ */ jsx("code", { className: "passflow-2fa-recovery-code", children: recoveryCode }, index)) }),
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
+ autoFocus: !isInputDisabled,
2444
+ autoComplete: "off",
2445
+ disabled: isInputDisabled
2446
+ }
2447
+ ),
2448
+ renderErrorContent()
2449
+ ] }) }),
2450
+ !isInputDisabled && /* @__PURE__ */ jsx(
2451
+ Button,
2452
+ {
2453
+ size: "big",
2454
+ type: "submit",
2455
+ variant: "primary",
2456
+ disabled: code.length < 8 || isLoading,
2457
+ className: "passflow-button-signin",
2458
+ children: isLoading ? "Verifying..." : "Verify"
2459
+ }
2460
+ ),
2461
+ onBack && !isInputDisabled && /* @__PURE__ */ jsx("div", { className: "passflow-form-actions", children: /* @__PURE__ */ jsxs("p", { className: "passflow-dont-have-account", children: [
2462
+ "Have your device?",
2463
+ " ",
2464
+ /* @__PURE__ */ jsx(
2465
+ "button",
2466
+ {
2467
+ type: "button",
2468
+ onClick: onBack,
2469
+ className: "passflow-link",
2470
+ style: { background: "none", border: "none", cursor: "pointer" },
2471
+ children: "Use authenticator code"
2472
+ }
2473
+ )
2474
+ ] }) })
2475
+ ] }) });
2476
+ };
2477
+
2478
+ const TwoFactorVerifyFlow = ({
2479
+ successAuthRedirect,
2480
+ signInPath = routes.signin.path,
2481
+ twoFactorSetupPath = routes.two_factor_setup?.path
2482
+ }) => {
2483
+ const [mode, setMode] = useState("verify");
2484
+ const [shouldBlockRender, setShouldBlockRender] = useState(false);
2485
+ const { navigate } = useNavigation();
2486
+ const passflow = usePassflow();
2487
+ const { errorType } = useTwoFactorVerify();
2488
+ useEffect(() => {
2489
+ if (!passflow.isTwoFactorVerificationRequired()) {
2490
+ if (TwoFactorLoopPrevention.canRedirect()) {
2491
+ TwoFactorLoopPrevention.incrementRedirect();
2492
+ navigate({ to: signInPath });
2493
+ setShouldBlockRender(true);
2494
+ } else {
2495
+ setShouldBlockRender(false);
2496
+ }
2497
+ }
2498
+ }, [passflow, navigate, signInPath]);
2499
+ useEffect(() => {
2500
+ if (errorType === "expired") {
2501
+ if (TwoFactorLoopPrevention.canRedirect()) {
2502
+ TwoFactorLoopPrevention.incrementRedirect();
2503
+ TwoFactorLoopPrevention.setLastErrorType("expired");
2504
+ setTimeout(() => {
2505
+ navigate({ to: signInPath });
2506
+ }, 1500);
2507
+ }
2508
+ } else if (errorType === "not_enabled") {
2509
+ TwoFactorLoopPrevention.setLastErrorType("not_enabled");
2510
+ }
2511
+ }, [errorType, navigate, signInPath]);
2512
+ const handleSuccess = async () => {
2513
+ TwoFactorLoopPrevention.reset();
2514
+ if (successAuthRedirect) {
2515
+ if (!isValidUrl(successAuthRedirect)) {
2516
+ navigate({ to: successAuthRedirect });
2517
+ } else {
2518
+ window.location.href = await getUrlWithTokens(passflow, successAuthRedirect);
2519
+ }
2520
+ }
2521
+ };
2522
+ const handleUseRecovery = () => {
2523
+ setMode("recovery");
2524
+ };
2525
+ const handleBackToVerify = () => {
2526
+ setMode("verify");
2527
+ };
2528
+ if (shouldBlockRender) {
2529
+ return null;
2530
+ }
2531
+ if (!passflow.isTwoFactorVerificationRequired() && TwoFactorLoopPrevention.canRedirect()) {
2532
+ return null;
2533
+ }
2534
+ return /* @__PURE__ */ jsx(Fragment, { children: mode === "verify" ? /* @__PURE__ */ jsx(
2535
+ TwoFactorVerifyForm,
2536
+ {
2537
+ onSuccess: handleSuccess,
2538
+ onUseRecovery: handleUseRecovery,
2539
+ signInPath,
2540
+ twoFactorSetupPath
2541
+ }
2542
+ ) : /* @__PURE__ */ jsx(TwoFactorRecoveryForm, { onSuccess: handleSuccess, onBack: handleBackToVerify, signInPath }) });
1453
2543
  };
1454
2544
 
1455
2545
  const defaultErrorMessage = "Something went wrong";
@@ -1479,13 +2569,25 @@ const ErrorComponent = ({ error = defaultErrorMessage, goBackRedirectTo }) => {
1479
2569
  );
1480
2570
  };
1481
2571
 
1482
- const excludeRoutes = ["verify-challenge-otp", "password/reset"];
2572
+ const excludeRoutes = ["verify-challenge-otp", "password/reset", "two-factor-verify", "two-factor-recovery"];
1483
2573
  const withError = (Component, ErrorComponent) => (props) => {
1484
2574
  const context = useContext(PassflowContext);
1485
2575
  const { successAuthRedirect } = props;
1486
2576
  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";
2577
+ if (excludeRoutes.some((route) => pathname.includes(route))) {
2578
+ return /* @__PURE__ */ jsx(
2579
+ ErrorBoundary,
2580
+ {
2581
+ FallbackComponent: ({ error }) => /* @__PURE__ */ jsx(ErrorComponent, { goBackRedirectTo: successAuthRedirect ?? "/", error: error.message }),
2582
+ children: /* @__PURE__ */ jsx(Component, { ...props })
2583
+ }
2584
+ );
2585
+ }
2586
+ if (context?.state.isDiscoveringAppId) {
2587
+ return null;
2588
+ }
2589
+ if (!context?.state.appId) {
2590
+ const errorMessage = "Missing appId";
1489
2591
  return /* @__PURE__ */ jsx(ErrorComponent, { goBackRedirectTo: successAuthRedirect ?? "/", error: errorMessage });
1490
2592
  }
1491
2593
  return /* @__PURE__ */ jsx(
@@ -1508,7 +2610,8 @@ const SignInForm = ({
1508
2610
  signUpPath = routes.signup.path,
1509
2611
  verifyOTPPath = routes.verify_otp.path,
1510
2612
  verifyMagicLinkPath = routes.verify_magic_link.path,
1511
- forgotPasswordPath = routes.forgot_password.path
2613
+ forgotPasswordPath = routes.forgot_password.path,
2614
+ twoFactorVerifyPath = routes.two_factor_verify.path
1512
2615
  }) => {
1513
2616
  const {
1514
2617
  getValues,
@@ -1523,7 +2626,16 @@ const SignInForm = ({
1523
2626
  });
1524
2627
  const passflow = usePassflow();
1525
2628
  const { navigate } = useNavigation();
1526
- const { appSettings, scopes, createTenantForNewUser, passwordPolicy, currentStyles, isError: isErrorApp, error: errorApp, loginAppTheme } = useAppSettings();
2629
+ const {
2630
+ appSettings,
2631
+ scopes,
2632
+ createTenantForNewUser,
2633
+ passwordPolicy,
2634
+ currentStyles,
2635
+ isError: isErrorApp,
2636
+ error: errorApp,
2637
+ loginAppTheme
2638
+ } = useAppSettings();
1527
2639
  const { federatedWithRedirect } = useProvider(successAuthRedirect, createTenantForNewUser);
1528
2640
  if (isErrorApp) throw new Error(errorApp);
1529
2641
  const authMethods = useMemo(() => getAuthMethods(appSettings?.auth_strategies), [appSettings]);
@@ -1576,7 +2688,12 @@ const SignInForm = ({
1576
2688
  };
1577
2689
  const status = await fetch(payload, "password");
1578
2690
  if (status) {
1579
- if (!isValidUrl(successAuthRedirect ?? appSettings.defaults.redirect)) navigate({ to: successAuthRedirect ?? appSettings.defaults.redirect });
2691
+ if (passflow.isTwoFactorVerificationRequired()) {
2692
+ navigate({ to: twoFactorVerifyPath ?? routes.two_factor_verify.path });
2693
+ return;
2694
+ }
2695
+ if (!isValidUrl(successAuthRedirect ?? appSettings.defaults.redirect))
2696
+ navigate({ to: successAuthRedirect ?? appSettings.defaults.redirect });
1580
2697
  else window.location.href = await getUrlWithTokens(passflow, successAuthRedirect ?? appSettings.defaults.redirect);
1581
2698
  }
1582
2699
  };
@@ -1587,7 +2704,12 @@ const SignInForm = ({
1587
2704
  };
1588
2705
  const response = await fetch(payload, "passkey");
1589
2706
  if (response) {
1590
- if (!isValidUrl(successAuthRedirect ?? appSettings.defaults.redirect)) navigate({ to: successAuthRedirect ?? appSettings.defaults.redirect });
2707
+ if (passflow.isTwoFactorVerificationRequired()) {
2708
+ navigate({ to: twoFactorVerifyPath ?? routes.two_factor_verify.path });
2709
+ return;
2710
+ }
2711
+ if (!isValidUrl(successAuthRedirect ?? appSettings.defaults.redirect))
2712
+ navigate({ to: successAuthRedirect ?? appSettings.defaults.redirect });
1591
2713
  else window.location.href = await getUrlWithTokens(passflow, successAuthRedirect ?? appSettings.defaults.redirect);
1592
2714
  }
1593
2715
  };
@@ -1966,7 +3088,16 @@ const SignUpForm = ({
1966
3088
  });
1967
3089
  const passflow = usePassflow();
1968
3090
  const { navigate } = useNavigation();
1969
- const { appSettings, scopes, createTenantForNewUser, passwordPolicy, currentStyles, isError: isErrorApp, error: errorApp, loginAppTheme } = useAppSettings();
3091
+ const {
3092
+ appSettings,
3093
+ scopes,
3094
+ createTenantForNewUser,
3095
+ passwordPolicy,
3096
+ currentStyles,
3097
+ isError: isErrorApp,
3098
+ error: errorApp,
3099
+ loginAppTheme
3100
+ } = useAppSettings();
1970
3101
  if (isErrorApp) throw new Error(errorApp);
1971
3102
  const { federatedWithRedirect } = useProvider(successAuthRedirect, createTenantForNewUser);
1972
3103
  const authMethods = useMemo(() => getAuthMethods(appSettings?.auth_strategies), [appSettings]);
@@ -2020,7 +3151,8 @@ const SignUpForm = ({
2020
3151
  };
2021
3152
  const status = await fetch(payload, "password");
2022
3153
  if (status) {
2023
- if (!isValidUrl(successAuthRedirect ?? appSettings.defaults.redirect)) navigate({ to: successAuthRedirect ?? appSettings.defaults.redirect });
3154
+ if (!isValidUrl(successAuthRedirect ?? appSettings.defaults.redirect))
3155
+ navigate({ to: successAuthRedirect ?? appSettings.defaults.redirect });
2024
3156
  else window.location.href = await getUrlWithTokens(passflow, successAuthRedirect ?? appSettings.defaults.redirect);
2025
3157
  }
2026
3158
  };
@@ -2035,7 +3167,8 @@ const SignUpForm = ({
2035
3167
  };
2036
3168
  const response = await fetch(payload, "passkey");
2037
3169
  if (response) {
2038
- if (!isValidUrl(successAuthRedirect ?? appSettings.defaults.redirect)) navigate({ to: successAuthRedirect ?? appSettings.defaults.redirect });
3170
+ if (!isValidUrl(successAuthRedirect ?? appSettings.defaults.redirect))
3171
+ navigate({ to: successAuthRedirect ?? appSettings.defaults.redirect });
2039
3172
  else window.location.href = await getUrlWithTokens(passflow, successAuthRedirect ?? appSettings.defaults.redirect);
2040
3173
  }
2041
3174
  };
@@ -2443,66 +3576,6 @@ const VerifyChallengeMagicLink = () => {
2443
3576
  );
2444
3577
  };
2445
3578
 
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
3579
  const TimerButton = ({ totalSecond, onClick, className = "", ...props }) => {
2507
3580
  const [seconds, setSeconds] = useState(totalSecond);
2508
3581
  useEffect(() => {
@@ -2544,6 +3617,24 @@ const TimerButton = ({ totalSecond, onClick, className = "", ...props }) => {
2544
3617
  );
2545
3618
  };
2546
3619
 
3620
+ const VerifyChallengeSuccess = () => {
3621
+ const { currentStyles, loginAppTheme } = useAppSettings();
3622
+ return /* @__PURE__ */ jsx(
3623
+ Wrapper,
3624
+ {
3625
+ iconId: "logo",
3626
+ className: "passflow-verify-challenge-success-wrapper",
3627
+ customCss: currentStyles?.custom_css,
3628
+ customLogo: currentStyles?.logo_url,
3629
+ removeBranding: loginAppTheme?.remove_passflow_logo,
3630
+ children: /* @__PURE__ */ jsx("div", { className: "passflow-verify-challenge-success-wrapper", children: /* @__PURE__ */ jsxs("div", { className: "passflow-verify-challenge-success-container", children: [
3631
+ /* @__PURE__ */ jsx("p", { className: "passflow-verify-challenge-success-text", children: "Successful verification!" }),
3632
+ /* @__PURE__ */ jsx("p", { className: "passflow-verify-challenge-success-text-secondary", children: "But there is no redirect URL for further redirect." })
3633
+ ] }) })
3634
+ }
3635
+ );
3636
+ };
3637
+
2547
3638
  const challengeTypeFullString = {
2548
3639
  email: "email address",
2549
3640
  phone: "phone number"
@@ -2667,6 +3758,48 @@ const VerifyChallengeOTPManual = ({
2667
3758
  );
2668
3759
  };
2669
3760
 
3761
+ const VerifyChallengeOTPRedirect = ({ otp, challengeId, appId }) => {
3762
+ const passflow = usePassflow();
3763
+ const { navigate } = useNavigation();
3764
+ const [paramsError, setParamsError] = useState(null);
3765
+ const [showSuccessMessage, setShowSuccessMessage] = useState(false);
3766
+ const { fetch, isError, error, isLoading } = usePasswordlessComplete();
3767
+ useEffect(() => {
3768
+ const fetchData = async () => {
3769
+ if (!appId) {
3770
+ setParamsError("Missing required param: app_id");
3771
+ return;
3772
+ }
3773
+ if (!otp) {
3774
+ setParamsError("Missing required param: otp");
3775
+ return;
3776
+ }
3777
+ if (!challengeId) {
3778
+ setParamsError("Missing required param: challenge_id");
3779
+ return;
3780
+ }
3781
+ if (!isLoading) {
3782
+ const response = await fetch({ otp, challenge_id: challengeId });
3783
+ if (response) {
3784
+ if (response.redirect_url) {
3785
+ if (!isValidUrl(response.redirect_url)) navigate({ to: response.redirect_url });
3786
+ else window.location.href = await getUrlWithTokens(passflow, response.redirect_url);
3787
+ } else {
3788
+ setShowSuccessMessage(true);
3789
+ }
3790
+ } else {
3791
+ setParamsError("Something went wrong. Please try again later.");
3792
+ }
3793
+ }
3794
+ };
3795
+ void fetchData();
3796
+ }, []);
3797
+ if (isError && error) throw new Error(error);
3798
+ if (paramsError) throw new Error(paramsError);
3799
+ if (showSuccessMessage) return /* @__PURE__ */ jsx(VerifyChallengeSuccess, {});
3800
+ return null;
3801
+ };
3802
+
2670
3803
  const redirectSearchParamsVerifyChallengeOtpSchema = Yup.object().shape({
2671
3804
  appId: Yup.string().required(),
2672
3805
  challengeId: Yup.string().required(),
@@ -3007,7 +4140,11 @@ const ResetPassword = ({ successAuthRedirect }) => {
3007
4140
  if (status) {
3008
4141
  if (!isValidUrl(resetTokenType?.redirect_url ?? successAuthRedirect ?? appSettings.defaults.redirect))
3009
4142
  navigate({ to: resetTokenType?.redirect_url ?? successAuthRedirect ?? appSettings.defaults.redirect });
3010
- else window.location.href = await getUrlWithTokens(passflow, resetTokenType?.redirect_url ?? successAuthRedirect ?? appSettings.defaults.redirect);
4143
+ else
4144
+ window.location.href = await getUrlWithTokens(
4145
+ passflow,
4146
+ resetTokenType?.redirect_url ?? successAuthRedirect ?? appSettings.defaults.redirect
4147
+ );
3011
4148
  }
3012
4149
  };
3013
4150
  const handlePasswordChange = async () => {
@@ -3140,7 +4277,7 @@ const InvitationJoinFlow = ({
3140
4277
  tenant_name: tenantName,
3141
4278
  redirect_url: redirectUrl
3142
4279
  } = invitationTokenData;
3143
- const parsedTokenCache = passflow.getParsedTokenCache();
4280
+ const parsedTokenCache = passflow.getParsedTokens();
3144
4281
  const onClickNavigateToSignInHandler = () => navigate({ to: signInPath, search: window.location.search });
3145
4282
  const onClickNavigateToSignUpHandler = () => navigate({ to: signUpPath, search: window.location.search });
3146
4283
  if (!parsedTokenCache?.access_token) onClickNavigateToSignInHandler();
@@ -4245,6 +5382,13 @@ function useNavigateUnstable() {
4245
5382
  }, [basename, navigator, routePathnamesJson, locationPathname, dataRouterContext]);
4246
5383
  return navigate;
4247
5384
  }
5385
+ function useParams() {
5386
+ let {
5387
+ matches
5388
+ } = React.useContext(RouteContext);
5389
+ let routeMatch = matches[matches.length - 1];
5390
+ return routeMatch ? routeMatch.params : {};
5391
+ }
4248
5392
  function useRoutes(routes, locationArg) {
4249
5393
  return useRoutesImpl(routes, locationArg);
4250
5394
  }
@@ -4498,13 +5642,13 @@ function _renderMatches(matches, parentMatches, dataRouterState, future) {
4498
5642
  }) : getChildren();
4499
5643
  }, null);
4500
5644
  }
4501
- var DataRouterHook$1 = /* @__PURE__ */ function(DataRouterHook2) {
5645
+ var DataRouterHook$1 = /* @__PURE__ */ (function(DataRouterHook2) {
4502
5646
  DataRouterHook2["UseBlocker"] = "useBlocker";
4503
5647
  DataRouterHook2["UseRevalidator"] = "useRevalidator";
4504
5648
  DataRouterHook2["UseNavigateStable"] = "useNavigate";
4505
5649
  return DataRouterHook2;
4506
- }(DataRouterHook$1 || {});
4507
- var DataRouterStateHook$1 = /* @__PURE__ */ function(DataRouterStateHook2) {
5650
+ })(DataRouterHook$1 || {});
5651
+ var DataRouterStateHook$1 = /* @__PURE__ */ (function(DataRouterStateHook2) {
4508
5652
  DataRouterStateHook2["UseBlocker"] = "useBlocker";
4509
5653
  DataRouterStateHook2["UseLoaderData"] = "useLoaderData";
4510
5654
  DataRouterStateHook2["UseActionData"] = "useActionData";
@@ -4516,7 +5660,7 @@ var DataRouterStateHook$1 = /* @__PURE__ */ function(DataRouterStateHook2) {
4516
5660
  DataRouterStateHook2["UseNavigateStable"] = "useNavigate";
4517
5661
  DataRouterStateHook2["UseRouteId"] = "useRouteId";
4518
5662
  return DataRouterStateHook2;
4519
- }(DataRouterStateHook$1 || {});
5663
+ })(DataRouterStateHook$1 || {});
4520
5664
  function useDataRouterContext(hookName) {
4521
5665
  let ctx = React.useContext(DataRouterContext);
4522
5666
  !ctx ? invariant(false) : void 0;
@@ -4541,8 +5685,8 @@ function useCurrentRouteId(hookName) {
4541
5685
  function useRouteError() {
4542
5686
  var _state$errors;
4543
5687
  let error = React.useContext(RouteErrorContext);
4544
- let state = useDataRouterState();
4545
- let routeId = useCurrentRouteId();
5688
+ let state = useDataRouterState(DataRouterStateHook$1.UseRouteError);
5689
+ let routeId = useCurrentRouteId(DataRouterStateHook$1.UseRouteError);
4546
5690
  if (error !== void 0) {
4547
5691
  return error;
4548
5692
  }
@@ -4888,6 +6032,7 @@ const index = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
4888
6032
  useInRouterContext,
4889
6033
  useLocation,
4890
6034
  useNavigate,
6035
+ useParams,
4891
6036
  useRouteError,
4892
6037
  useRoutes
4893
6038
  }, Symbol.toStringTag, { value: 'Module' }));
@@ -4975,7 +6120,8 @@ const PassflowWrapper = ({
4975
6120
  signUpPath: routesWithPrefix.signup,
4976
6121
  forgotPasswordPath: routesWithPrefix.forgot_password,
4977
6122
  verifyMagicLinkPath: routesWithPrefix.verify_magic_link,
4978
- verifyOTPPath: routesWithPrefix.verify_otp
6123
+ verifyOTPPath: routesWithPrefix.verify_otp,
6124
+ twoFactorVerifyPath: routesWithPrefix.two_factor_verify
4979
6125
  }
4980
6126
  )
4981
6127
  }
@@ -5043,6 +6189,34 @@ const PassflowWrapper = ({
5043
6189
  )
5044
6190
  }
5045
6191
  ),
6192
+ /* @__PURE__ */ jsx(
6193
+ Route,
6194
+ {
6195
+ path: routesWithPrefix.two_factor_verify,
6196
+ element: /* @__PURE__ */ jsx(
6197
+ TwoFactorVerifyFlow,
6198
+ {
6199
+ successAuthRedirect,
6200
+ signInPath: routesWithPrefix.signin,
6201
+ twoFactorSetupPath: routesWithPrefix.two_factor_setup
6202
+ }
6203
+ )
6204
+ }
6205
+ ),
6206
+ /* @__PURE__ */ jsx(
6207
+ Route,
6208
+ {
6209
+ path: routesWithPrefix.two_factor_setup,
6210
+ element: /* @__PURE__ */ jsx(
6211
+ TwoFactorSetupFlow,
6212
+ {
6213
+ successAuthRedirect,
6214
+ twoFactorVerifyPath: routesWithPrefix.two_factor_verify,
6215
+ signInPath: routesWithPrefix.signin
6216
+ }
6217
+ )
6218
+ }
6219
+ ),
5046
6220
  /* @__PURE__ */ jsx(
5047
6221
  Route,
5048
6222
  {
@@ -5062,15 +6236,116 @@ const PassflowWrapper = ({
5062
6236
  };
5063
6237
  const PassflowFlow = withError(PassflowWrapper, ErrorComponent);
5064
6238
 
6239
+ const TwoFactorSetupMagicLinkFlow = ({
6240
+ onSuccess,
6241
+ onError,
6242
+ redirectOnSuccess = true,
6243
+ signInPath = routes.signin.path
6244
+ }) => {
6245
+ const { token } = useParams();
6246
+ const { navigate } = useNavigation();
6247
+ const { isLoading, isRetrying, isValidated, error, retry, retryCountdown } = useTwoFactorSetupMagicLink(token || "");
6248
+ useEffect(() => {
6249
+ if (error) {
6250
+ onError?.(error);
6251
+ }
6252
+ }, [error, onError]);
6253
+ const handleSetupComplete = () => {
6254
+ onSuccess?.();
6255
+ if (redirectOnSuccess) {
6256
+ navigate({
6257
+ to: signInPath
6258
+ });
6259
+ }
6260
+ };
6261
+ const handleCancel = () => {
6262
+ navigate({ to: signInPath });
6263
+ };
6264
+ if (isLoading) {
6265
+ return /* @__PURE__ */ jsx(
6266
+ Wrapper,
6267
+ {
6268
+ title: "Validating Magic Link",
6269
+ subtitle: "Please wait while we verify your link...",
6270
+ className: "passflow-2fa-magic-link-loading",
6271
+ children: /* @__PURE__ */ jsxs("div", { className: "passflow-loading-container", role: "status", "aria-live": "polite", "aria-busy": "true", children: [
6272
+ /* @__PURE__ */ jsx(Icon, { id: "logo", type: "general", size: "large", className: "passflow-loading-spinner" }),
6273
+ /* @__PURE__ */ jsx("p", { className: "passflow-loading-text", children: "Validating your magic link..." })
6274
+ ] })
6275
+ }
6276
+ );
6277
+ }
6278
+ if (error) {
6279
+ const isRetryable = error.code === "SERVER_ERROR" || error.code === "RATE_LIMITED";
6280
+ const isRateLimited = error.code === "RATE_LIMITED" && retryCountdown !== null && retryCountdown > 0;
6281
+ return /* @__PURE__ */ jsx(
6282
+ Wrapper,
6283
+ {
6284
+ title: "Magic Link Validation Failed",
6285
+ subtitle: getErrorSubtitle(error.code),
6286
+ className: "passflow-2fa-magic-link-error",
6287
+ children: /* @__PURE__ */ jsxs("div", { className: "passflow-error-container", role: "alert", "aria-live": "assertive", children: [
6288
+ /* @__PURE__ */ jsx("div", { className: "passflow-error-icon", children: /* @__PURE__ */ jsx(Icon, { id: "warning", type: "general", size: "large" }) }),
6289
+ /* @__PURE__ */ jsx("p", { className: "passflow-error-message", id: "error-message", children: error.message }),
6290
+ /* @__PURE__ */ jsxs("div", { className: "passflow-error-actions", children: [
6291
+ isRetryable && /* @__PURE__ */ jsx(
6292
+ Button,
6293
+ {
6294
+ size: "big",
6295
+ type: "button",
6296
+ variant: "primary",
6297
+ onClick: retry,
6298
+ disabled: isRateLimited || isRetrying,
6299
+ "aria-describedby": "error-message",
6300
+ children: isRetrying ? "Retrying..." : isRateLimited ? `Try again in ${formatCountdown(retryCountdown)}` : "Try Again"
6301
+ }
6302
+ ),
6303
+ /* @__PURE__ */ jsx(Button, { size: "big", type: "button", variant: "secondary", onClick: () => navigate({ to: signInPath }), children: "Back to Sign In" })
6304
+ ] }),
6305
+ !isRetryable && /* @__PURE__ */ jsx("p", { className: "passflow-error-help-text", children: "Please contact your administrator to request a new magic link." })
6306
+ ] })
6307
+ }
6308
+ );
6309
+ }
6310
+ if (isValidated) {
6311
+ return /* @__PURE__ */ jsx(TwoFactorSetupForm, { onComplete: handleSetupComplete, onCancel: handleCancel });
6312
+ }
6313
+ return null;
6314
+ };
6315
+ function getErrorSubtitle(code) {
6316
+ switch (code) {
6317
+ case "EXPIRED_TOKEN":
6318
+ return "This magic link has expired";
6319
+ case "INVALID_TOKEN":
6320
+ return "This magic link is invalid";
6321
+ case "REVOKED_TOKEN":
6322
+ return "This magic link has been revoked";
6323
+ case "RATE_LIMITED":
6324
+ return "Too many attempts";
6325
+ case "SERVER_ERROR":
6326
+ return "Something went wrong";
6327
+ default:
6328
+ return "An error occurred";
6329
+ }
6330
+ }
6331
+ function formatCountdown(seconds) {
6332
+ if (seconds === null) return "";
6333
+ const mins = Math.floor(seconds / 60);
6334
+ const secs = seconds % 60;
6335
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
6336
+ }
6337
+
5065
6338
  const PassflowProvider = ({
5066
6339
  children,
5067
6340
  navigate: initialNavigate,
5068
6341
  router = "default",
5069
6342
  ...config
5070
6343
  }) => {
6344
+ const needsDiscovery = !config.appId;
5071
6345
  const [state, dispatch] = useReducer(passflowReducer, {
5072
6346
  ...initialState,
5073
- ...config
6347
+ ...config,
6348
+ isDiscoveringAppId: needsDiscovery
5074
6349
  });
5075
6350
  const [navigate, setNavigate] = useState(() => {
5076
6351
  if (initialNavigate) {
@@ -5079,6 +6354,52 @@ const PassflowProvider = ({
5079
6354
  return defaultNavigate;
5080
6355
  });
5081
6356
  const passflow = useMemo(() => new Passflow(state), [state]);
6357
+ const discoveryAttemptedRef = useRef(false);
6358
+ useEffect(() => {
6359
+ if (needsDiscovery && !discoveryAttemptedRef.current && state.isDiscoveringAppId) {
6360
+ discoveryAttemptedRef.current = true;
6361
+ const discoverAppId = async () => {
6362
+ const urlsToTry = ["/settings"];
6363
+ if (config.url) {
6364
+ urlsToTry.push(`${config.url}/settings`);
6365
+ }
6366
+ for (const url of urlsToTry) {
6367
+ try {
6368
+ const response = await fetch(url);
6369
+ if (response.ok) {
6370
+ const settings = await response.json();
6371
+ const discoveredAppId = settings.login_app?.app_id || settings.appId;
6372
+ if (discoveredAppId) {
6373
+ passflow.setAppId(discoveredAppId);
6374
+ dispatch({
6375
+ type: "SET_PASSFLOW_STATE",
6376
+ payload: {
6377
+ ...state,
6378
+ appId: discoveredAppId,
6379
+ scopes: settings.login_app?.scopes || settings.scopes || state.scopes,
6380
+ createTenantForNewUser: settings.login_app?.create_tenant_for_new_user ?? settings.createTenantForNewUser ?? state.createTenantForNewUser,
6381
+ isDiscoveringAppId: false
6382
+ }
6383
+ });
6384
+ return;
6385
+ }
6386
+ }
6387
+ } catch (error) {
6388
+ continue;
6389
+ }
6390
+ }
6391
+ console.warn("Failed to discover appId from /settings");
6392
+ dispatch({
6393
+ type: "SET_PASSFLOW_STATE",
6394
+ payload: {
6395
+ ...state,
6396
+ isDiscoveringAppId: false
6397
+ }
6398
+ });
6399
+ };
6400
+ void discoverAppId();
6401
+ }
6402
+ }, [needsDiscovery, state.isDiscoveringAppId, config.url, passflow, state]);
5082
6403
  const passflowValue = useMemo(() => ({ state, dispatch, passflow }), [state, passflow]);
5083
6404
  const handleSetNavigate = useCallback((newNavigate) => {
5084
6405
  setNavigate(() => newNavigate || defaultNavigate);
@@ -5094,5 +6415,5 @@ const PassflowProvider = ({
5094
6415
  return /* @__PURE__ */ jsx(PassflowContext.Provider, { value: passflowValue, children: /* @__PURE__ */ jsx(NavigationContext$1.Provider, { value: navigationValue, children: /* @__PURE__ */ jsx(AuthProvider, { children }) }) });
5095
6416
  };
5096
6417
 
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 };
6418
+ 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
6419
  //# sourceMappingURL=index.es.js.map