@abstraxn/signer-react 2.1.5 → 2.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 (44) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/src/WalletModal.css +0 -1
  3. package/dist/src/WalletModal.js +10 -4
  4. package/dist/src/WalletModal.js.map +1 -1
  5. package/dist/src/components/AbstraxnProvider/AbstraxnProviderInner.js +557 -81
  6. package/dist/src/components/AbstraxnProvider/AbstraxnProviderInner.js.map +1 -1
  7. package/dist/src/components/AbstraxnProvider/useOAuthCallbacks.d.ts +2 -1
  8. package/dist/src/components/AbstraxnProvider/useOAuthCallbacks.js +72 -8
  9. package/dist/src/components/AbstraxnProvider/useOAuthCallbacks.js.map +1 -1
  10. package/dist/src/components/AbstraxnProvider/useWalletInitialization.js +16 -11
  11. package/dist/src/components/AbstraxnProvider/useWalletInitialization.js.map +1 -1
  12. package/dist/src/components/AbstraxnProvider/utils.d.ts +5 -0
  13. package/dist/src/components/AbstraxnProvider/utils.js +9 -0
  14. package/dist/src/components/AbstraxnProvider/utils.js.map +1 -1
  15. package/dist/src/components/OnboardingUI/OnboardingUI.css +186 -6
  16. package/dist/src/components/OnboardingUI/OnboardingUIReact.js +18 -4
  17. package/dist/src/components/OnboardingUI/OnboardingUIReact.js.map +1 -1
  18. package/dist/src/components/OnboardingUI/OnboardingUIWeb.d.ts +21 -0
  19. package/dist/src/components/OnboardingUI/OnboardingUIWeb.js +528 -20
  20. package/dist/src/components/OnboardingUI/OnboardingUIWeb.js.map +1 -1
  21. package/dist/src/components/OnboardingUI/components/EmailForm.js +2 -1
  22. package/dist/src/components/OnboardingUI/components/EmailForm.js.map +1 -1
  23. package/dist/src/components/OnboardingUI/components/MfaForm.d.ts +17 -0
  24. package/dist/src/components/OnboardingUI/components/MfaForm.js +81 -0
  25. package/dist/src/components/OnboardingUI/components/MfaForm.js.map +1 -0
  26. package/dist/src/components/OnboardingUI/components/index.d.ts +2 -0
  27. package/dist/src/components/OnboardingUI/components/index.js +1 -0
  28. package/dist/src/components/OnboardingUI/components/index.js.map +1 -1
  29. package/dist/src/components/OnboardingUI/hooks/useAuthMethods.js +3 -0
  30. package/dist/src/components/OnboardingUI/hooks/useAuthMethods.js.map +1 -1
  31. package/dist/src/components/OnboardingUI/hooks/useOnboarding.d.ts +4 -1
  32. package/dist/src/components/OnboardingUI/hooks/useOnboarding.js +65 -2
  33. package/dist/src/components/OnboardingUI/hooks/useOnboarding.js.map +1 -1
  34. package/dist/src/components/WalletModal/components/ManageWalletModal.css +1443 -2
  35. package/dist/src/components/WalletModal/components/ManageWalletModal.js +737 -23
  36. package/dist/src/components/WalletModal/components/ManageWalletModal.js.map +1 -1
  37. package/dist/src/components/WalletModal/components/ReceiveModal.css +0 -1
  38. package/dist/src/components/WalletModal/hooks/useSendTransaction.js +67 -12
  39. package/dist/src/components/WalletModal/hooks/useSendTransaction.js.map +1 -1
  40. package/dist/src/types.d.ts +16 -0
  41. package/dist/src/wagmiConfig.js +1 -0
  42. package/dist/src/wagmiConfig.js.map +1 -1
  43. package/dist/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +3 -2
@@ -11,6 +11,7 @@ import { parseEther, createPublicClient, http } from "viem";
11
11
  import { ExternalWalletButtons } from "../../ExternalWalletButtons";
12
12
  import { EVM_CHAINS, SOLANA_CHAINS, getChainById, toCoreChain, } from "../../chains";
13
13
  import { AbstraxnContext } from "./context";
14
+ import { isLinkAuthCallback } from "./utils";
14
15
  /** Syncs wagmi disconnect to external wallet state; only rendered when wagmi is available (under WagmiProvider). */
15
16
  function WagmiConnectionEffectSync({ onDisconnect, }) {
16
17
  useConnectionEffect({ onDisconnect });
@@ -47,6 +48,72 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
47
48
  useEffect(() => {
48
49
  isExternalWalletConnectedRef.current = isExternalWalletConnected;
49
50
  }, [isExternalWalletConnected]);
51
+ // Detect link-auth error in popup: notify opener and close (run first so popup closes on error)
52
+ useEffect(() => {
53
+ if (typeof window === "undefined" || !window.opener)
54
+ return;
55
+ const urlParams = new URLSearchParams(window.location.search);
56
+ const errorParam = urlParams.get("error");
57
+ if (!errorParam)
58
+ return;
59
+ let provider = "google";
60
+ try {
61
+ const linkingMetadataRaw = urlParams.get("linkingMetadata");
62
+ if (linkingMetadataRaw) {
63
+ const linkingMetadata = JSON.parse(decodeURIComponent(linkingMetadataRaw));
64
+ const authProvider = linkingMetadata?.authProvider;
65
+ if (authProvider) {
66
+ provider = String(authProvider).toLowerCase();
67
+ }
68
+ }
69
+ }
70
+ catch {
71
+ // keep default provider
72
+ }
73
+ let errorMessage;
74
+ try {
75
+ errorMessage = decodeURIComponent(errorParam);
76
+ }
77
+ catch {
78
+ errorMessage = errorParam;
79
+ }
80
+ try {
81
+ window.opener.postMessage({ type: "abstraxn-auth-link-error", provider, error: errorMessage }, window.location.origin);
82
+ }
83
+ finally {
84
+ window.close();
85
+ }
86
+ }, []);
87
+ // Detect link-auth callback in popup: notify opener and close (run early, no wallet dependency)
88
+ useEffect(() => {
89
+ if (typeof window === "undefined" || !window.opener)
90
+ return;
91
+ const urlParams = new URLSearchParams(window.location.search);
92
+ const stampingRequired = urlParams.get("stampingRequired") === "true";
93
+ const activityBody = urlParams.get("activityBody");
94
+ if (!stampingRequired || !activityBody)
95
+ return;
96
+ let provider = "google";
97
+ try {
98
+ const linkingMetadataRaw = urlParams.get("linkingMetadata");
99
+ if (linkingMetadataRaw) {
100
+ const linkingMetadata = JSON.parse(decodeURIComponent(linkingMetadataRaw));
101
+ const authProvider = linkingMetadata?.authProvider;
102
+ if (authProvider) {
103
+ provider = String(authProvider).toLowerCase();
104
+ }
105
+ }
106
+ }
107
+ catch {
108
+ // keep default provider
109
+ }
110
+ try {
111
+ window.opener.postMessage({ type: "abstraxn-auth-link-done", provider, activityBody }, window.location.origin);
112
+ }
113
+ finally {
114
+ window.close();
115
+ }
116
+ }, []);
50
117
  // Track Solana state changes
51
118
  useEffect(() => {
52
119
  if (solana?.wallet?.connected && solana?.wallet?.publicKey) {
@@ -156,20 +223,37 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
156
223
  return;
157
224
  requestAnimationFrame(() => {
158
225
  if (externalWalletsEnabled) {
159
- // Show external wallet container when enabled
160
- if (onboardingAny.externalWalletContainer) {
226
+ // Don't show external wallet on OTP or MFA (auth code) screen
227
+ const isOtpScreen = onboardingAny.otpVerificationScreen &&
228
+ onboardingAny.otpVerificationScreen.parentElement &&
229
+ onboardingAny.otpVerificationScreen.offsetParent !== null;
230
+ const isMfaScreen = onboardingAny.mfaVerificationScreen &&
231
+ onboardingAny.mfaVerificationScreen.parentElement &&
232
+ onboardingAny.mfaVerificationScreen.offsetParent !== null;
233
+ if (!isOtpScreen && !isMfaScreen && onboardingAny.externalWalletContainer) {
161
234
  onboardingAny.externalWalletContainer.style.display = "";
162
235
  }
163
- // Show divider if email/Google are also visible
164
- const authMethods = onboardingAny.config?.authMethods || [
165
- "otp",
166
- "google",
167
- ];
168
- const showEmail = authMethods.includes("otp");
169
- const showGoogle = authMethods.includes("google");
170
- const hasEmailOrGoogle = showEmail || showGoogle;
171
- if (onboardingAny.externalWalletDivider && hasEmailOrGoogle) {
172
- onboardingAny.externalWalletDivider.style.display = "";
236
+ else if (onboardingAny.externalWalletContainer) {
237
+ onboardingAny.externalWalletContainer.style.display = "none";
238
+ }
239
+ // Show divider only if email/Google are also visible (and not on OTP/MFA)
240
+ if (!isOtpScreen && !isMfaScreen) {
241
+ const authMethods = onboardingAny.config?.authMethods || [
242
+ "otp",
243
+ "google",
244
+ ];
245
+ const showEmail = authMethods.includes("otp");
246
+ const showGoogle = authMethods.includes("google");
247
+ const hasEmailOrGoogle = showEmail || showGoogle;
248
+ if (onboardingAny.externalWalletDivider && hasEmailOrGoogle) {
249
+ onboardingAny.externalWalletDivider.style.display = "";
250
+ }
251
+ else if (onboardingAny.externalWalletDivider) {
252
+ onboardingAny.externalWalletDivider.style.display = "none";
253
+ }
254
+ }
255
+ else if (onboardingAny.externalWalletDivider) {
256
+ onboardingAny.externalWalletDivider.style.display = "none";
173
257
  }
174
258
  }
175
259
  else {
@@ -513,6 +597,12 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
513
597
  const authManager = walletInstance.getAuthManager();
514
598
  const user = await authManager.verifyOTP(otpIdRef.current, otp);
515
599
  otpIdRef.current = null;
600
+ const lastAuthState = authManager.getLastAuthState();
601
+ if (lastAuthState?.mfaRequired === true) {
602
+ // Show MFA verification screen; do not connect until MFA is verified
603
+ setError(null);
604
+ return { success: true, user, mfaRequired: true };
605
+ }
516
606
  // Connect wallet after successful authentication
517
607
  await walletInstance.connect();
518
608
  // Clear any previous errors on successful verification
@@ -526,6 +616,51 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
526
616
  return { success: false, error: errorMessage };
527
617
  }
528
618
  },
619
+ onMfaVerify: async (code) => {
620
+ try {
621
+ if (!walletInstance)
622
+ throw new Error("Wallet not initialized");
623
+ await walletInstance.verifyMfa(code);
624
+ await walletInstance.connect();
625
+ setError(null);
626
+ // Explicitly refresh provider state so OnboardingUI / app re-renders as connected without relying on connect event
627
+ try {
628
+ const whoamiInfo = await walletInstance.getWhoami();
629
+ if (whoamiInfo) {
630
+ setIsConnected(true);
631
+ const userInfo = await walletInstance.getUserInfo();
632
+ setUser(userInfo);
633
+ setWhoami(whoamiInfo);
634
+ try {
635
+ const addr = await walletInstance.getAddress();
636
+ setAddress(addr);
637
+ }
638
+ catch (_e) { /* optional */ }
639
+ try {
640
+ const cid = await walletInstance.getChainId();
641
+ setChainId(cid);
642
+ }
643
+ catch (_e) { /* optional */ }
644
+ }
645
+ }
646
+ catch (refreshErr) {
647
+ console.warn("Post-MFA state refresh:", refreshErr);
648
+ }
649
+ // Clear OAuth callback params from URL so app shows connected state without refresh
650
+ if (typeof window !== "undefined") {
651
+ const url = new URL(window.location.href);
652
+ const keys = ["success", "mfaRequired", "user", "accessToken", "refreshToken", "turnkeyPublicKey", "error", "provider", "authProvider"];
653
+ keys.forEach((k) => url.searchParams.delete(k));
654
+ window.history.replaceState({}, document.title, url.pathname + url.search);
655
+ }
656
+ return { success: true };
657
+ }
658
+ catch (err) {
659
+ console.error("MFA Verify Error:", err);
660
+ const errorMessage = err instanceof Error ? err.message : "MFA verification failed";
661
+ return { success: false, error: errorMessage };
662
+ }
663
+ },
529
664
  onGoogleLogin: async () => {
530
665
  if (!walletInstance)
531
666
  throw new Error("Wallet not initialized");
@@ -535,6 +670,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
535
670
  try {
536
671
  localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
537
672
  localStorage.setItem("abstraxn_oauth_pending", "google");
673
+ localStorage.setItem("abstraxn_oauth_ui", "modal");
538
674
  }
539
675
  catch (e) {
540
676
  // Ignore localStorage errors
@@ -563,6 +699,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
563
699
  try {
564
700
  localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
565
701
  localStorage.setItem("abstraxn_oauth_pending", "twitter");
702
+ localStorage.setItem("abstraxn_oauth_ui", "modal");
566
703
  }
567
704
  catch (e) {
568
705
  // Ignore localStorage errors
@@ -591,6 +728,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
591
728
  try {
592
729
  localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
593
730
  localStorage.setItem("abstraxn_oauth_pending", "discord");
731
+ localStorage.setItem("abstraxn_oauth_ui", "modal");
594
732
  }
595
733
  catch (e) {
596
734
  // Ignore localStorage errors
@@ -651,6 +789,16 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
651
789
  onLoginSuccess: async (_data) => {
652
790
  // Clear any previous errors on successful login
653
791
  setError(null);
792
+ // Clear URL params (e.g. after OAuth MFA verification so success/user/accessToken are removed)
793
+ if (typeof window !== "undefined") {
794
+ window.history.replaceState({}, document.title, window.location.pathname);
795
+ }
796
+ // Set user from wallet (covers OAuth MFA path where we did not call setUser in the callback)
797
+ if (walletInstance) {
798
+ const currentUser = walletInstance.getAuthManager().getCurrentUser();
799
+ if (currentUser)
800
+ setUser(currentUser);
801
+ }
654
802
  // Restore connection type from localStorage for OAuth logins (Google, X, Discord)
655
803
  // This ensures connectionType is set after OAuth redirects
656
804
  try {
@@ -804,16 +952,18 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
804
952
  onboardingAny.modalOverlay.style.display = "none";
805
953
  document.body.style.overflow = "";
806
954
  }
807
- else if (hasSuccess) {
808
- // If success=true is in URL, show loading modal
809
- // Use setTimeout to ensure onboarding is fully initialized
810
- setTimeout(() => {
811
- const onboardingInstance = onboardingRef.current;
812
- if (onboardingInstance && onboardingInstance.showLoadingModal) {
813
- // Directly show loading modal - it creates its own overlay with header and footer
814
- onboardingInstance.showLoadingModal();
815
- }
816
- }, 150);
955
+ else if (hasSuccess && !isLinkAuthCallback(urlParams)) {
956
+ // If success=true is in URL (login flow only), show loading modal. Skip for link-auth callback (popup handles it).
957
+ // Skip when mfaRequired=true so MFA verification screen can show instead.
958
+ const mfaRequired = urlParams.get("mfaRequired") === "true";
959
+ if (!mfaRequired) {
960
+ setTimeout(() => {
961
+ const onboardingInstance = onboardingRef.current;
962
+ if (onboardingInstance && onboardingInstance.showLoadingModal) {
963
+ onboardingInstance.showLoadingModal();
964
+ }
965
+ }, 150);
966
+ }
817
967
  }
818
968
  onboardingRef.current = onboarding;
819
969
  // Handle Google OAuth callback (only in useEffect, not exposed)
@@ -821,6 +971,8 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
821
971
  if (googleCallbackHandledRef.current)
822
972
  return;
823
973
  const urlParams = new URLSearchParams(window.location.search);
974
+ if (isLinkAuthCallback(urlParams))
975
+ return;
824
976
  if (!hasAuthParams(urlParams) ||
825
977
  matchesProvider("twitter", urlParams) ||
826
978
  matchesProvider("discord", urlParams)) {
@@ -839,9 +991,9 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
839
991
  localStorage.removeItem("abstraxn_oauth_pending");
840
992
  }
841
993
  catch (e) { }
842
- // Show loading modal if success=true is in URL
843
994
  const hasSuccess = urlParams.get("success") === "true";
844
- if (hasSuccess && onboardingRef.current) {
995
+ const mfaRequired = urlParams.get("mfaRequired") === "true";
996
+ if (hasSuccess && !mfaRequired && onboardingRef.current) {
845
997
  const onboardingAny = onboardingRef.current;
846
998
  if (onboardingAny.showLoadingScreen) {
847
999
  onboardingAny.showLoadingScreen();
@@ -854,6 +1006,24 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
854
1006
  try {
855
1007
  const user = await walletInstance.handleGoogleCallback();
856
1008
  if (user) {
1009
+ const lastAuthState = walletInstance.getAuthManager().getLastAuthState();
1010
+ if (lastAuthState?.mfaRequired === true) {
1011
+ try {
1012
+ if (localStorage.getItem("abstraxn_oauth_ui") !== "inline") {
1013
+ showOnboarding();
1014
+ if (onboardingRef.current && typeof onboardingRef.current.showMfaVerificationScreenForOAuth === "function") {
1015
+ onboardingRef.current.showMfaVerificationScreenForOAuth();
1016
+ }
1017
+ }
1018
+ }
1019
+ finally {
1020
+ try {
1021
+ localStorage.removeItem("abstraxn_oauth_ui");
1022
+ }
1023
+ catch (e) { }
1024
+ }
1025
+ return;
1026
+ }
857
1027
  setUser(user);
858
1028
  window.history.replaceState({}, document.title, window.location.pathname);
859
1029
  // Hide loading modal and close modal on success
@@ -895,6 +1065,8 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
895
1065
  if (discordCallbackHandledRef.current)
896
1066
  return;
897
1067
  const urlParams = new URLSearchParams(window.location.search);
1068
+ if (isLinkAuthCallback(urlParams))
1069
+ return;
898
1070
  if (!hasAuthParams(urlParams) || !matchesProvider("discord", urlParams)) {
899
1071
  return;
900
1072
  }
@@ -911,9 +1083,10 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
911
1083
  localStorage.removeItem("abstraxn_oauth_pending");
912
1084
  }
913
1085
  catch (e) { }
914
- // Show loading modal if success=true is in URL
1086
+ // Show loading modal if success=true is in URL (skip when mfaRequired so MFA screen can show)
915
1087
  const hasSuccess = urlParams.get("success") === "true";
916
- if (hasSuccess && onboardingRef.current) {
1088
+ const mfaRequired = urlParams.get("mfaRequired") === "true";
1089
+ if (hasSuccess && !mfaRequired && onboardingRef.current) {
917
1090
  const onboardingAny = onboardingRef.current;
918
1091
  if (onboardingAny.showLoadingScreen) {
919
1092
  onboardingAny.showLoadingScreen();
@@ -926,6 +1099,24 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
926
1099
  try {
927
1100
  const user = await walletInstance.handleDiscordCallback();
928
1101
  if (user) {
1102
+ const lastAuthState = walletInstance.getAuthManager().getLastAuthState();
1103
+ if (lastAuthState?.mfaRequired === true) {
1104
+ try {
1105
+ if (localStorage.getItem("abstraxn_oauth_ui") !== "inline") {
1106
+ showOnboarding();
1107
+ if (onboardingRef.current && typeof onboardingRef.current.showMfaVerificationScreenForOAuth === "function") {
1108
+ onboardingRef.current.showMfaVerificationScreenForOAuth();
1109
+ }
1110
+ }
1111
+ }
1112
+ finally {
1113
+ try {
1114
+ localStorage.removeItem("abstraxn_oauth_ui");
1115
+ }
1116
+ catch (e) { }
1117
+ }
1118
+ return;
1119
+ }
929
1120
  setUser(user);
930
1121
  window.history.replaceState({}, document.title, window.location.pathname);
931
1122
  // Hide loading modal and close modal on success
@@ -969,6 +1160,8 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
969
1160
  if (twitterCallbackHandledRef.current)
970
1161
  return;
971
1162
  const urlParams = new URLSearchParams(window.location.search);
1163
+ if (isLinkAuthCallback(urlParams))
1164
+ return;
972
1165
  if (!hasAuthParams(urlParams) || !matchesProvider("twitter", urlParams)) {
973
1166
  return;
974
1167
  }
@@ -985,9 +1178,10 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
985
1178
  localStorage.removeItem("abstraxn_oauth_pending");
986
1179
  }
987
1180
  catch (e) { }
988
- // Show loading modal if success=true is in URL
1181
+ // Show loading modal if success=true is in URL (skip when mfaRequired so MFA screen can show)
989
1182
  const hasSuccess = urlParams.get("success") === "true";
990
- if (hasSuccess && onboardingRef.current) {
1183
+ const mfaRequired = urlParams.get("mfaRequired") === "true";
1184
+ if (hasSuccess && !mfaRequired && onboardingRef.current) {
991
1185
  const onboardingAny = onboardingRef.current;
992
1186
  if (onboardingAny.showLoadingScreen) {
993
1187
  onboardingAny.showLoadingScreen();
@@ -1000,6 +1194,24 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
1000
1194
  try {
1001
1195
  const user = await walletInstance.handleTwitterCallback();
1002
1196
  if (user) {
1197
+ const lastAuthState = walletInstance.getAuthManager().getLastAuthState();
1198
+ if (lastAuthState?.mfaRequired === true) {
1199
+ try {
1200
+ if (localStorage.getItem("abstraxn_oauth_ui") !== "inline") {
1201
+ showOnboarding();
1202
+ if (onboardingRef.current && typeof onboardingRef.current.showMfaVerificationScreenForOAuth === "function") {
1203
+ onboardingRef.current.showMfaVerificationScreenForOAuth();
1204
+ }
1205
+ }
1206
+ }
1207
+ finally {
1208
+ try {
1209
+ localStorage.removeItem("abstraxn_oauth_ui");
1210
+ }
1211
+ catch (e) { }
1212
+ }
1213
+ return;
1214
+ }
1003
1215
  setUser(user);
1004
1216
  window.history.replaceState({}, document.title, window.location.pathname);
1005
1217
  // Hide loading modal and close modal on success
@@ -1133,13 +1345,15 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
1133
1345
  // Ensure external wallet container is visible when modal opens
1134
1346
  if (externalWalletsEnabledRef.current &&
1135
1347
  onboarding.externalWalletContainer) {
1136
- // Check if we're on OTP screen - if so, don't show external wallets
1137
- // Check if OTP screen exists AND is actually visible in the DOM
1348
+ // Check if we're on OTP or MFA screen - if so, don't show external wallets
1138
1349
  const isOtpScreen = onboarding.otpVerificationScreen &&
1139
1350
  onboarding.otpVerificationScreen.parentElement &&
1140
1351
  onboarding.otpVerificationScreen.offsetParent !== null;
1141
- if (!isOtpScreen) {
1142
- // Only show external wallets if not on OTP screen
1352
+ const isMfaScreen = onboarding.mfaVerificationScreen &&
1353
+ onboarding.mfaVerificationScreen.parentElement &&
1354
+ onboarding.mfaVerificationScreen.offsetParent !== null;
1355
+ if (!isOtpScreen && !isMfaScreen) {
1356
+ // Only show external wallets if not on OTP or MFA (auth code) screen
1143
1357
  onboarding.externalWalletContainer.style.display = "";
1144
1358
  // Show divider only if both email/Google AND external wallets are visible
1145
1359
  const authMethods = onboarding.config?.authMethods || [
@@ -1157,7 +1371,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
1157
1371
  }
1158
1372
  }
1159
1373
  else {
1160
- // On OTP screen - hide external wallets
1374
+ // On OTP or MFA screen - hide external wallets
1161
1375
  onboarding.externalWalletContainer.style.display = "none";
1162
1376
  if (onboarding.externalWalletDivider) {
1163
1377
  onboarding.externalWalletDivider.style.display = "none";
@@ -1181,10 +1395,13 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
1181
1395
  // Modal already exists, just show it
1182
1396
  // Use requestAnimationFrame to prevent blinking
1183
1397
  requestAnimationFrame(() => {
1184
- // Check if we're on OTP screen - if so, ensure social buttons stay hidden
1398
+ // Check if we're on OTP or MFA screen - if so, ensure social buttons stay hidden
1185
1399
  const isOtpScreen = onboarding.otpVerificationScreen &&
1186
1400
  onboarding.otpVerificationScreen.parentElement &&
1187
1401
  onboarding.otpVerificationScreen.offsetParent !== null;
1402
+ const isMfaScreen = onboarding.mfaVerificationScreen &&
1403
+ onboarding.mfaVerificationScreen.parentElement &&
1404
+ onboarding.mfaVerificationScreen.offsetParent !== null;
1188
1405
  onboarding.modalOverlay.classList.remove("onboarding-modal-closing");
1189
1406
  onboarding.modalOverlay.classList.add("onboarding-modal-open");
1190
1407
  onboarding.modalOverlay.style.display = "flex";
@@ -1195,8 +1412,8 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
1195
1412
  if (emailForOtp && typeof onboarding.setPreFillEmail === "function") {
1196
1413
  onboarding.setPreFillEmail(emailForOtp, { readOnly: true });
1197
1414
  }
1198
- // If on OTP screen, ensure all login elements (social buttons, etc.) are hidden
1199
- if (isOtpScreen) {
1415
+ // If on OTP or MFA screen, ensure all login elements (social buttons, etc.) are hidden
1416
+ if (isOtpScreen || isMfaScreen) {
1200
1417
  // On OTP screen - ensure all login elements are hidden
1201
1418
  if (onboarding.hideLoginElements &&
1202
1419
  typeof onboarding.hideLoginElements === "function") {
@@ -1229,9 +1446,9 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
1229
1446
  // CRITICAL: Always ensure external wallets are mounted when modal reopens
1230
1447
  if (externalWalletsEnabledRef.current &&
1231
1448
  onboarding.externalWalletContainer) {
1232
- // Check if we're on OTP screen - if so, don't show external wallets
1233
- if (!isOtpScreen) {
1234
- // Only show external wallets if not on OTP screen
1449
+ // Check if we're on OTP or MFA screen - if so, don't show external wallets
1450
+ if (!isOtpScreen && !isMfaScreen) {
1451
+ // Only show external wallets if not on OTP or MFA (auth code) screen
1235
1452
  onboarding.externalWalletContainer.style.display = "";
1236
1453
  // Show divider only if both email/Google AND external wallets are visible
1237
1454
  const authMethods = onboarding.config?.authMethods || [
@@ -1249,7 +1466,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
1249
1466
  }
1250
1467
  }
1251
1468
  else {
1252
- // On OTP screen - hide external wallets
1469
+ // On OTP or MFA screen - hide external wallets
1253
1470
  onboarding.externalWalletContainer.style.display = "none";
1254
1471
  if (onboarding.externalWalletDivider) {
1255
1472
  onboarding.externalWalletDivider.style.display = "none";
@@ -2130,6 +2347,138 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
2130
2347
  setLoading(false);
2131
2348
  }
2132
2349
  }, [isExternalWalletConnected]);
2350
+ const getSupportedAuthMethods = useCallback(async () => {
2351
+ if (!walletRef.current?.isConnected) {
2352
+ throw new Error("Wallet not connected");
2353
+ }
2354
+ return await walletRef.current.getSupportedAuthMethods();
2355
+ }, []);
2356
+ const getLinkedAuthMethods = useCallback(async () => {
2357
+ if (!walletRef.current?.isConnected) {
2358
+ throw new Error("Wallet not connected");
2359
+ }
2360
+ return await walletRef.current.getLinkedAuthMethods();
2361
+ }, []);
2362
+ const linkAuthMethod = useCallback(async (provider) => {
2363
+ if (!walletRef.current?.isConnected) {
2364
+ throw new Error("Wallet not connected");
2365
+ }
2366
+ if (provider.toLowerCase() === "passkey") {
2367
+ await walletRef.current.linkPasskey();
2368
+ return;
2369
+ }
2370
+ if (provider.toLowerCase() === "email") {
2371
+ throw new Error("Email linking uses the in-page form. Use the Auth Methods screen and click Connect for Email, then enter your email and OTP.");
2372
+ }
2373
+ const { authUrl } = await walletRef.current.initiateLinkAuth(provider);
2374
+ const width = 500;
2375
+ const height = 600;
2376
+ const left = Math.round((window.screen.width - width) / 2);
2377
+ const top = Math.round((window.screen.height - height) / 2);
2378
+ const popup = window.open(authUrl, "abstraxn-auth-link", `width=${width},height=${height},left=${left},top=${top},scrollbars=yes,resizable=yes`);
2379
+ if (!popup) {
2380
+ throw new Error("Popup blocked. Please allow popups for this site.");
2381
+ }
2382
+ return new Promise((resolve, reject) => {
2383
+ let settled = false;
2384
+ const maybeSubmit = (data) => {
2385
+ if (settled)
2386
+ return;
2387
+ settled = true;
2388
+ cleanup();
2389
+ if (data?.activityBody) {
2390
+ walletRef.current
2391
+ ?.submitLinkAuthStamped(provider, data.activityBody)
2392
+ .then(resolve)
2393
+ .catch((err) => {
2394
+ settled = false;
2395
+ reject(err);
2396
+ });
2397
+ }
2398
+ else {
2399
+ reject(new Error("Linking was cancelled"));
2400
+ }
2401
+ };
2402
+ const handleMessage = (event) => {
2403
+ if (event.data?.type === "abstraxn-auth-link-done" &&
2404
+ event.data?.provider === provider) {
2405
+ maybeSubmit(event.data);
2406
+ return;
2407
+ }
2408
+ if (event.data?.type === "abstraxn-auth-link-error") {
2409
+ if (settled)
2410
+ return;
2411
+ settled = true;
2412
+ cleanup();
2413
+ reject(new Error(event.data.error));
2414
+ }
2415
+ };
2416
+ let intervalId = null;
2417
+ const cleanup = () => {
2418
+ window.removeEventListener("message", handleMessage);
2419
+ if (intervalId)
2420
+ clearInterval(intervalId);
2421
+ };
2422
+ window.addEventListener("message", handleMessage);
2423
+ intervalId = setInterval(() => {
2424
+ if (popup.closed) {
2425
+ cleanup();
2426
+ if (!settled) {
2427
+ reject(new Error("Linking was cancelled"));
2428
+ }
2429
+ }
2430
+ }, 200);
2431
+ });
2432
+ }, []);
2433
+ const unlinkAuthMethod = useCallback(async (provider) => {
2434
+ if (!walletRef.current?.isConnected) {
2435
+ throw new Error("Wallet not connected");
2436
+ }
2437
+ await walletRef.current.unlinkAuthMethod(provider);
2438
+ }, []);
2439
+ const requestOtpForEmailLink = useCallback(async (email) => {
2440
+ if (!walletRef.current?.isConnected) {
2441
+ throw new Error("Wallet not connected");
2442
+ }
2443
+ return await walletRef.current.requestOtpForEmailLink(email);
2444
+ }, []);
2445
+ const linkEmail = useCallback(async (otpId, otpCode) => {
2446
+ if (!walletRef.current?.isConnected) {
2447
+ throw new Error("Wallet not connected");
2448
+ }
2449
+ await walletRef.current.linkEmail(otpId, otpCode);
2450
+ }, []);
2451
+ // MFA helpers (delegating to core wallet/AuthManager)
2452
+ const getMfaStatus = useCallback(async () => {
2453
+ if (!walletRef.current?.isConnected) {
2454
+ throw new Error("Wallet not connected");
2455
+ }
2456
+ return await walletRef.current.getMfaStatus();
2457
+ }, []);
2458
+ const enableMfa = useCallback(async () => {
2459
+ if (!walletRef.current?.isConnected) {
2460
+ throw new Error("Wallet not connected");
2461
+ }
2462
+ return await walletRef.current.enableMfa();
2463
+ }, []);
2464
+ const verifySetupMfa = useCallback(async (code) => {
2465
+ if (!walletRef.current?.isConnected) {
2466
+ throw new Error("Wallet not connected");
2467
+ }
2468
+ return await walletRef.current.verifySetupMfa(code);
2469
+ }, []);
2470
+ const verifyMfa = useCallback(async (code) => {
2471
+ if (!walletRef.current?.isConnected) {
2472
+ throw new Error("Wallet not connected");
2473
+ }
2474
+ return await walletRef.current.verifyMfa(code);
2475
+ }, []);
2476
+ const disableMfaWithSignedPayload = useCallback(async () => {
2477
+ if (!walletRef.current?.isConnected) {
2478
+ throw new Error("Wallet not connected");
2479
+ }
2480
+ return await walletRef.current.disableMfaWithSignedPayload();
2481
+ }, []);
2133
2482
  // Sync external wallet state from wagmi
2134
2483
  useEffect(() => {
2135
2484
  let checkAddressTimeout = null;
@@ -2864,44 +3213,52 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
2864
3213
  const currentChainId = isExternalWalletConnected && externalWalletChainId
2865
3214
  ? externalWalletChainId
2866
3215
  : chainId;
2867
- // Fetch balance for Abstraxn wallet (not external wallet)
2868
- useEffect(() => {
3216
+ // Fetch balance for Abstraxn wallet (not external wallet) - reusable for on-demand refetch
3217
+ const refetchWalletBalance = useCallback(async () => {
2869
3218
  if (isExternalWalletConnected || !address || !currentChainId) {
2870
3219
  setWalletBalance(null);
2871
3220
  return;
2872
3221
  }
2873
- const fetchBalance = async () => {
2874
- try {
2875
- const currentChain = getChainById(currentChainId);
2876
- if (!currentChain || currentChain.type !== "evm") {
2877
- setWalletBalance(null);
2878
- return;
2879
- }
2880
- const publicClient = createPublicClient({
2881
- chain: {
2882
- id: currentChain.id,
2883
- name: currentChain.name,
2884
- nativeCurrency: currentChain.nativeCurrency,
2885
- rpcUrls: {
2886
- default: {
2887
- http: [currentChain.rpcUrl],
2888
- },
2889
- },
2890
- },
2891
- transport: http(currentChain.rpcUrl),
2892
- });
2893
- const balance = await publicClient.getBalance({
2894
- address: address,
2895
- });
2896
- setWalletBalance(balance);
2897
- }
2898
- catch (error) {
2899
- console.error("Failed to fetch balance:", error);
3222
+ try {
3223
+ const currentChain = getChainById(currentChainId);
3224
+ if (!currentChain || currentChain.type !== "evm") {
2900
3225
  setWalletBalance(null);
3226
+ return;
2901
3227
  }
2902
- };
2903
- fetchBalance();
3228
+ const publicClient = createPublicClient({
3229
+ chain: {
3230
+ id: currentChain.id,
3231
+ name: currentChain.name,
3232
+ nativeCurrency: currentChain.nativeCurrency,
3233
+ rpcUrls: {
3234
+ default: {
3235
+ http: [currentChain.rpcUrl],
3236
+ },
3237
+ },
3238
+ },
3239
+ transport: http(currentChain.rpcUrl),
3240
+ });
3241
+ const balance = await publicClient.getBalance({
3242
+ address: address,
3243
+ });
3244
+ setWalletBalance(balance);
3245
+ }
3246
+ catch (error) {
3247
+ console.error("Failed to fetch balance:", error);
3248
+ setWalletBalance(null);
3249
+ }
2904
3250
  }, [address, currentChainId, isExternalWalletConnected]);
3251
+ useEffect(() => {
3252
+ refetchWalletBalance();
3253
+ }, [refetchWalletBalance]);
3254
+ // Single refetch for UI: Abstraxn wallet = RPC call, external wallet = wagmi refetch
3255
+ const refetchBalance = useCallback(() => {
3256
+ if (isExternalWalletConnected && wagmiBalance?.refetch) {
3257
+ void wagmiBalance.refetch();
3258
+ return;
3259
+ }
3260
+ void refetchWalletBalance();
3261
+ }, [isExternalWalletConnected, wagmiBalance, refetchWalletBalance]);
2905
3262
  // Compute available chains from config - supports both legacy and new format
2906
3263
  const availableChains = useMemo(() => {
2907
3264
  const chains = [];
@@ -3042,6 +3399,109 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3042
3399
  const setEmailForOtp = useCallback((email) => {
3043
3400
  setEmailForOtpState(email);
3044
3401
  }, []);
3402
+ // Enriched uiConfig with auth callbacks so <OnboardingUI /> (OnboardingUIReact) gets full flow including MFA
3403
+ const enrichedUiConfig = useMemo(() => ({
3404
+ ...config.ui,
3405
+ onEmailOtpInitiate: async (email) => {
3406
+ try {
3407
+ if (!walletRef.current)
3408
+ throw new Error("Wallet not initialized");
3409
+ const authManager = walletRef.current.getAuthManager();
3410
+ const connectionTypeValue = "email";
3411
+ setConnectionType(connectionTypeValue);
3412
+ try {
3413
+ localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
3414
+ }
3415
+ catch (e) {
3416
+ // Ignore
3417
+ }
3418
+ const result = await authManager.loginWithOTP(email);
3419
+ otpIdRef.current = result.otpId;
3420
+ }
3421
+ catch (err) {
3422
+ console.error("OTP Init Error:", err);
3423
+ throw err;
3424
+ }
3425
+ },
3426
+ onEmailOtpVerify: async (_email, otp) => {
3427
+ try {
3428
+ if (!walletRef.current)
3429
+ throw new Error("Wallet not initialized");
3430
+ if (!otpIdRef.current)
3431
+ throw new Error("OTP ID not found");
3432
+ const connectionTypeValue = "email";
3433
+ setConnectionType(connectionTypeValue);
3434
+ try {
3435
+ localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
3436
+ }
3437
+ catch (e) {
3438
+ // Ignore
3439
+ }
3440
+ const authManager = walletRef.current.getAuthManager();
3441
+ const user = await authManager.verifyOTP(otpIdRef.current, otp);
3442
+ otpIdRef.current = null;
3443
+ const lastAuthState = authManager.getLastAuthState();
3444
+ if (lastAuthState?.mfaRequired === true) {
3445
+ setError(null);
3446
+ return { success: true, user, mfaRequired: true };
3447
+ }
3448
+ await walletRef.current.connect();
3449
+ setError(null);
3450
+ return { success: true, user };
3451
+ }
3452
+ catch (err) {
3453
+ console.error("OTP Verify Error:", err);
3454
+ const errorMessage = err instanceof Error ? err.message : "Failed to verify OTP";
3455
+ return { success: false, error: errorMessage };
3456
+ }
3457
+ },
3458
+ onMfaVerify: async (code) => {
3459
+ try {
3460
+ if (!walletRef.current)
3461
+ throw new Error("Wallet not initialized");
3462
+ const wallet = walletRef.current;
3463
+ await wallet.verifyMfa(code);
3464
+ await wallet.connect();
3465
+ setError(null);
3466
+ // Explicitly refresh provider state so OnboardingUI / app re-renders as connected without relying on connect event
3467
+ try {
3468
+ const whoamiInfo = await wallet.getWhoami();
3469
+ if (whoamiInfo) {
3470
+ setIsConnected(true);
3471
+ const userInfo = await wallet.getUserInfo();
3472
+ setUser(userInfo);
3473
+ setWhoami(whoamiInfo);
3474
+ try {
3475
+ const addr = await wallet.getAddress();
3476
+ setAddress(addr);
3477
+ }
3478
+ catch (_e) { /* optional */ }
3479
+ try {
3480
+ const cid = await wallet.getChainId();
3481
+ setChainId(cid);
3482
+ }
3483
+ catch (_e) { /* optional */ }
3484
+ }
3485
+ }
3486
+ catch (refreshErr) {
3487
+ console.warn("Post-MFA state refresh:", refreshErr);
3488
+ }
3489
+ // Clear OAuth callback params from URL so app shows connected state without refresh
3490
+ if (typeof window !== "undefined") {
3491
+ const url = new URL(window.location.href);
3492
+ const keys = ["success", "mfaRequired", "user", "accessToken", "refreshToken", "turnkeyPublicKey", "error", "provider", "authProvider"];
3493
+ keys.forEach((k) => url.searchParams.delete(k));
3494
+ window.history.replaceState({}, document.title, url.pathname + url.search);
3495
+ }
3496
+ return { success: true };
3497
+ }
3498
+ catch (err) {
3499
+ console.error("MFA Verify Error:", err);
3500
+ const errorMessage = err instanceof Error ? err.message : "MFA verification failed";
3501
+ return { success: false, error: errorMessage };
3502
+ }
3503
+ },
3504
+ }), [config.ui]);
3045
3505
  const value = {
3046
3506
  wallet: walletRef.current,
3047
3507
  isInitialized,
@@ -3076,7 +3536,18 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3076
3536
  loginWithGoogle,
3077
3537
  handleGoogleCallback: () => handleGoogleCallback(true),
3078
3538
  refreshWhoami,
3079
- uiConfig: config.ui,
3539
+ getMfaStatus,
3540
+ enableMfa,
3541
+ verifySetupMfa,
3542
+ verifyMfa,
3543
+ disableMfaWithSignedPayload,
3544
+ getSupportedAuthMethods,
3545
+ getLinkedAuthMethods,
3546
+ linkAuthMethod,
3547
+ requestOtpForEmailLink,
3548
+ linkEmail,
3549
+ unlinkAuthMethod,
3550
+ uiConfig: enrichedUiConfig,
3080
3551
  // External wallet methods
3081
3552
  connectExternalWallet: externalWalletsEnabled
3082
3553
  ? connectExternalWallet
@@ -3115,6 +3586,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3115
3586
  availableChains,
3116
3587
  // Balance (for Abstraxn wallet)
3117
3588
  walletBalance: !isExternalWalletConnected ? walletBalance : undefined,
3589
+ refetchBalance,
3118
3590
  // Connection type
3119
3591
  connectionType,
3120
3592
  // Pre-fill email for email-OTP flow
@@ -3142,12 +3614,15 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3142
3614
  if (!container) {
3143
3615
  return;
3144
3616
  }
3145
- // Check if we're on OTP screen - if so, don't show external wallets
3617
+ // Check if we're on OTP or MFA screen - if so, don't show external wallets
3146
3618
  const isOtpScreen = onboardingAny.otpVerificationScreen &&
3147
3619
  onboardingAny.otpVerificationScreen.parentElement &&
3148
3620
  onboardingAny.otpVerificationScreen.offsetParent !== null;
3149
- if (!isOtpScreen) {
3150
- // Only show external wallets if not on OTP screen
3621
+ const isMfaScreen = onboardingAny.mfaVerificationScreen &&
3622
+ onboardingAny.mfaVerificationScreen.parentElement &&
3623
+ onboardingAny.mfaVerificationScreen.offsetParent !== null;
3624
+ if (!isOtpScreen && !isMfaScreen) {
3625
+ // Only show external wallets if not on OTP or MFA (auth code) screen
3151
3626
  container.style.display = "";
3152
3627
  // Show divider only if both email/Google AND external wallets are visible
3153
3628
  const authMethods = onboardingAny.config?.authMethods || [
@@ -3165,7 +3640,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3165
3640
  }
3166
3641
  }
3167
3642
  else {
3168
- // On OTP screen - hide external wallets
3643
+ // On OTP or MFA screen - hide external wallets
3169
3644
  container.style.display = "none";
3170
3645
  if (onboardingAny.externalWalletDivider) {
3171
3646
  onboardingAny.externalWalletDivider.style.display = "none";
@@ -3330,14 +3805,15 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, solana, }
3330
3805
  console.warn("External wallet container is null");
3331
3806
  return;
3332
3807
  }
3333
- // Check if we're on OTP screen - if so, don't show external wallets
3334
- // Check if OTP screen exists AND is actually visible in the DOM
3808
+ // Check if we're on OTP or MFA screen - if so, don't show external wallets
3335
3809
  const isOtpScreen = onboardingAny.otpVerificationScreen &&
3336
3810
  onboardingAny.otpVerificationScreen.parentElement &&
3337
3811
  onboardingAny.otpVerificationScreen.offsetParent !== null;
3338
- if (!isOtpScreen) {
3339
- // Only show external wallets if not on OTP screen
3340
- // Force container to be visible when external wallets are enabled
3812
+ const isMfaScreen = onboardingAny.mfaVerificationScreen &&
3813
+ onboardingAny.mfaVerificationScreen.parentElement &&
3814
+ onboardingAny.mfaVerificationScreen.offsetParent !== null;
3815
+ if (!isOtpScreen && !isMfaScreen) {
3816
+ // Only show external wallets if not on OTP or MFA (auth code) screen
3341
3817
  container.style.display = "";
3342
3818
  // Show divider only if both email/Google AND external wallets are visible
3343
3819
  const authMethods = onboardingAny.config?.authMethods || [