@b3dotfun/sdk 0.1.68-alpha.8 → 0.1.68-alpha.9

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 (28) hide show
  1. package/dist/cjs/global-account/react/components/SignInWithB3/SignIn.js +1 -1
  2. package/dist/cjs/global-account/react/components/SignInWithB3/SignInWithB3Flow.js +13 -5
  3. package/dist/cjs/global-account/react/components/SignInWithB3/SignInWithB3Privy.js +1 -1
  4. package/dist/cjs/global-account/react/components/SignInWithB3/steps/LoginStep.d.ts +1 -1
  5. package/dist/cjs/global-account/react/components/SignInWithB3/steps/LoginStep.js +21 -24
  6. package/dist/cjs/global-account/react/components/SignInWithB3/steps/LoginStepCustom.js +1 -1
  7. package/dist/cjs/global-account/react/hooks/useAuthentication.d.ts +3 -1
  8. package/dist/cjs/global-account/react/hooks/useAuthentication.js +51 -7
  9. package/dist/cjs/global-account/react/hooks/useGetAllTWSigners.js +2 -1
  10. package/dist/esm/global-account/react/components/SignInWithB3/SignIn.js +1 -1
  11. package/dist/esm/global-account/react/components/SignInWithB3/SignInWithB3Flow.js +13 -5
  12. package/dist/esm/global-account/react/components/SignInWithB3/SignInWithB3Privy.js +1 -1
  13. package/dist/esm/global-account/react/components/SignInWithB3/steps/LoginStep.d.ts +1 -1
  14. package/dist/esm/global-account/react/components/SignInWithB3/steps/LoginStep.js +21 -24
  15. package/dist/esm/global-account/react/components/SignInWithB3/steps/LoginStepCustom.js +1 -1
  16. package/dist/esm/global-account/react/hooks/useAuthentication.d.ts +3 -1
  17. package/dist/esm/global-account/react/hooks/useAuthentication.js +51 -7
  18. package/dist/esm/global-account/react/hooks/useGetAllTWSigners.js +2 -1
  19. package/dist/types/global-account/react/components/SignInWithB3/steps/LoginStep.d.ts +1 -1
  20. package/dist/types/global-account/react/hooks/useAuthentication.d.ts +3 -1
  21. package/package.json +1 -1
  22. package/src/global-account/react/components/SignInWithB3/SignIn.tsx +1 -1
  23. package/src/global-account/react/components/SignInWithB3/SignInWithB3Flow.tsx +13 -5
  24. package/src/global-account/react/components/SignInWithB3/SignInWithB3Privy.tsx +1 -1
  25. package/src/global-account/react/components/SignInWithB3/steps/LoginStep.tsx +35 -25
  26. package/src/global-account/react/components/SignInWithB3/steps/LoginStepCustom.tsx +1 -1
  27. package/src/global-account/react/hooks/useAuthentication.ts +51 -8
  28. package/src/global-account/react/hooks/useGetAllTWSigners.tsx +2 -1
@@ -19,7 +19,7 @@ function SignIn(props) {
19
19
  const { address: globalAddress, ensName, connectedSmartWallet, connectedEOAWallet, isActiveSmartWallet, isActiveEOAWallet, smartWalletIcon, } = (0, react_1.useAccountWallet)();
20
20
  const { data: walletImage } = (0, react_4.useWalletImage)(connectedEOAWallet?.id);
21
21
  const isMobile = (0, react_1.useIsMobile)();
22
- const { logout } = (0, react_1.useAuthentication)(partnerId);
22
+ const { logout } = (0, react_1.useAuthentication)(partnerId, { skipAutoConnect: true });
23
23
  const onDisconnect = async () => {
24
24
  await logout();
25
25
  };
@@ -17,16 +17,23 @@ const MAX_REFETCH_ATTEMPTS = 20;
17
17
  */
18
18
  function SignInWithB3Flow({ strategies, onLoginSuccess, onSessionKeySuccess, onError, chain, sessionKeyAddress, partnerId, closeAfterLogin = false, source = "signInWithB3Button", signersEnabled = false, }) {
19
19
  const { automaticallySetFirstEoa } = (0, react_1.useB3Config)();
20
- const { user, logout } = (0, react_1.useAuthentication)(partnerId);
21
- // FIXME Logout before login to ensure a clean state
20
+ // skipAutoConnect: this component intentionally logs out on mount to show a fresh login screen.
21
+ // AuthenticationProvider is the sole owner of useAutoConnect to avoid competing auth cycles.
22
+ const { user, logout } = (0, react_1.useAuthentication)(partnerId, { skipAutoConnect: true });
23
+ // Tracks whether the pre-login logout has finished.
24
+ // We must not render ConnectEmbed until logout (wallet disconnect) completes,
25
+ // otherwise the wallet state disrupts ConnectEmbed causing a blank modal.
26
+ const [readyToShowLogin, setReadyToShowLogin] = (0, react_2.useState)(source === "requestPermissions");
22
27
  const hasLoggedOutRef = (0, react_2.useRef)(false);
23
28
  (0, react_2.useEffect)(() => {
24
29
  if (hasLoggedOutRef.current)
25
30
  return;
26
31
  if (source !== "requestPermissions") {
27
32
  debug("Logging out before login");
28
- logout();
29
33
  hasLoggedOutRef.current = true;
34
+ logout().finally(() => {
35
+ setReadyToShowLogin(true);
36
+ });
30
37
  }
31
38
  }, [source, logout]);
32
39
  const [step, setStep] = (0, react_2.useState)(source === "requestPermissions" ? null : "login");
@@ -231,8 +238,9 @@ function SignInWithB3Flow({ strategies, onLoginSuccess, onSessionKeySuccess, onE
231
238
  content = ((0, jsx_runtime_1.jsx)(LoginStep_1.LoginStepContainer, { partnerId: partnerId, children: (0, jsx_runtime_1.jsx)("div", { className: "p-4 text-center text-red-500", children: refetchError }) }));
232
239
  }
233
240
  else if (step === "login") {
234
- // Show loading spinner
235
- if (isAuthenticating || (isFetchingSigners && step === "login") || source === "requestPermissions") {
241
+ // Show loading spinner while: authenticating, waiting for pre-login logout to finish,
242
+ // or fetching signers.
243
+ if (!readyToShowLogin || isAuthenticating || isFetchingSigners) {
236
244
  content = ((0, jsx_runtime_1.jsx)(LoginStep_1.LoginStepContainer, { partnerId: partnerId, children: (0, jsx_runtime_1.jsx)("div", { className: "my-8 flex min-h-[350px] items-center justify-center", children: (0, jsx_runtime_1.jsx)(react_1.Loading, { variant: "white", size: "lg" }) }) }));
237
245
  }
238
246
  else {
@@ -11,7 +11,7 @@ function SignInWithB3Privy({ onSuccess, onError, chain }) {
11
11
  const { isLoading, connectTw, fullToken } = (0, react_1.useHandleConnectWithPrivy)(chain, onSuccess);
12
12
  const setIsAuthenticating = (0, react_1.useAuthStore)(state => state.setIsAuthenticating);
13
13
  const setIsAuthenticated = (0, react_1.useAuthStore)(state => state.setIsAuthenticated);
14
- const { logout } = (0, react_1.useAuthentication)(partnerId);
14
+ const { logout } = (0, react_1.useAuthentication)(partnerId, { skipAutoConnect: true });
15
15
  debug("@@SignInWithB3Privy", {
16
16
  isLoading,
17
17
  fullToken,
@@ -19,5 +19,5 @@ interface LoginStepContainerProps {
19
19
  partnerId?: string;
20
20
  }
21
21
  export declare function LoginStepContainer({ children, partnerId }: LoginStepContainerProps): import("react/jsx-runtime").JSX.Element;
22
- export declare function LoginStep({ onSuccess, chain }: LoginStepProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function LoginStep({ onSuccess, chain }: LoginStepProps): import("react/jsx-runtime").JSX.Element | null;
23
23
  export {};
@@ -7,6 +7,7 @@ const react_1 = require("../../../../../global-account/react");
7
7
  const constants_1 = require("../../../../../shared/constants");
8
8
  const thirdweb_1 = require("../../../../../shared/utils/thirdweb");
9
9
  const react_2 = require("thirdweb/react");
10
+ const react_3 = require("react");
10
11
  const wallets_1 = require("thirdweb/wallets");
11
12
  function LoginStepContainer({ children, partnerId }) {
12
13
  const { data: partner } = (0, react_1.useQueryB3)("global-accounts-partners", "find", {
@@ -18,30 +19,17 @@ function LoginStepContainer({ children, partnerId }) {
18
19
  const partnerLogo = partner?.data?.[0]?.loginCustomization?.logoUrl;
19
20
  return ((0, jsx_runtime_1.jsxs)("div", { className: "bg-b3-react-background flex flex-col items-center justify-center pt-6", children: [partnerLogo && ((0, jsx_runtime_1.jsx)("img", { src: partnerLogo, alt: "Partner Logo", className: "partner-logo mb-6 h-12 w-auto object-contain" })), children] }));
20
21
  }
21
- function LoginStep({ onSuccess, chain }) {
22
- const { partnerId, theme } = (0, react_1.useB3Config)();
23
- const wallet = (0, wallets_1.ecosystemWallet)(constants_1.ecosystemWalletId, {
24
- partnerId: partnerId,
25
- });
26
- const { onConnect } = (0, react_1.useAuthentication)(partnerId);
27
- return ((0, jsx_runtime_1.jsx)(LoginStepContainer, { partnerId: partnerId, children: (0, jsx_runtime_1.jsx)(react_2.ConnectEmbed, { showThirdwebBranding: false, client: thirdweb_1.client, chain: chain, wallets: [wallet], theme: theme === "light"
28
- ? (0, react_2.lightTheme)({
29
- colors: {
30
- modalBg: "hsl(var(--b3-react-background))",
31
- },
32
- })
33
- : (0, react_2.darkTheme)({
34
- colors: {
35
- modalBg: "hsl(var(--b3-react-background))",
36
- },
37
- }), style: {
38
- width: "100%",
39
- height: "100%",
40
- border: 0,
41
- }, header: {
42
- title: "Sign in with B3",
43
- titleIcon: "https://cdn.b3.fun/b3_logo.svg",
44
- }, className: "b3-login-step", onConnect: async (wallet, allConnectedWallets) => {
22
+ /** Inner component that only mounts when partnerId is a non-empty string.
23
+ * Keeps all hooks unconditional without calling useAuthentication(""). */
24
+ function LoginStepContent({ onSuccess, chain, partnerId, theme, }) {
25
+ const wallet = (0, react_3.useMemo)(() => (0, wallets_1.ecosystemWallet)(constants_1.ecosystemWalletId, { partnerId }), [partnerId]);
26
+ // skipAutoConnect: AuthenticationProvider already owns the auto-connect instance.
27
+ // Creating another here would cause a second authentication cycle (another 401 attempt)
28
+ // that makes the modal flash between spinner and blank before finally showing the login form.
29
+ const { onConnect } = (0, react_1.useAuthentication)(partnerId, { skipAutoConnect: true });
30
+ return ((0, jsx_runtime_1.jsx)(LoginStepContainer, { partnerId: partnerId, children: (0, jsx_runtime_1.jsx)(react_2.ConnectEmbed, { showThirdwebBranding: false, autoConnect: false, client: thirdweb_1.client, chain: chain, wallets: [wallet], theme: theme === "light"
31
+ ? (0, react_2.lightTheme)({ colors: { modalBg: "hsl(var(--b3-react-background))" } })
32
+ : (0, react_2.darkTheme)({ colors: { modalBg: "hsl(var(--b3-react-background))" } }), style: { width: "100%", height: "100%", border: 0 }, header: { title: "Sign in with B3", titleIcon: "https://cdn.b3.fun/b3_logo.svg" }, className: "b3-login-step", onConnect: async (wallet, allConnectedWallets) => {
45
33
  await onConnect(wallet, allConnectedWallets);
46
34
  const account = wallet.getAccount();
47
35
  if (!account)
@@ -49,3 +37,12 @@ function LoginStep({ onSuccess, chain }) {
49
37
  await onSuccess(account);
50
38
  } }) }));
51
39
  }
40
+ function LoginStep({ onSuccess, chain }) {
41
+ const { partnerId, theme } = (0, react_1.useB3Config)();
42
+ // partnerId may be undefined during the brief B3Provider hydration window.
43
+ // Return null rather than rendering ConnectEmbed with an invalid ecosystem
44
+ // wallet config (which causes a blank screen).
45
+ if (!partnerId)
46
+ return null;
47
+ return (0, jsx_runtime_1.jsx)(LoginStepContent, { onSuccess: onSuccess, chain: chain, partnerId: partnerId, theme: theme });
48
+ }
@@ -16,7 +16,7 @@ function LoginStepCustom({ onSuccess, onError, chain, strategies, maxInitialWall
16
16
  const { connect } = (0, react_1.useConnect)(partnerId, chain);
17
17
  const setIsAuthenticating = (0, react_1.useAuthStore)(state => state.setIsAuthenticating);
18
18
  const setIsAuthenticated = (0, react_1.useAuthStore)(state => state.setIsAuthenticated);
19
- const { logout } = (0, react_1.useAuthentication)(partnerId);
19
+ const { logout } = (0, react_1.useAuthentication)(partnerId, { skipAutoConnect: true });
20
20
  const { connect: connectTW } = (0, react_3.useConnect)();
21
21
  // Split strategies into auth and wallet types
22
22
  const authStrategies = strategies.filter(s => !(0, react_1.isWalletType)(s));
@@ -1,6 +1,8 @@
1
1
  import { Wallet } from "thirdweb/wallets";
2
2
  import { preAuthenticate } from "thirdweb/wallets/in-app";
3
- export declare function useAuthentication(partnerId: string): {
3
+ export declare function useAuthentication(partnerId: string, { skipAutoConnect }?: {
4
+ skipAutoConnect?: boolean;
5
+ }): {
4
6
  logout: (callback?: () => void) => Promise<void>;
5
7
  isAuthenticated: boolean;
6
8
  isReady: boolean;
@@ -21,11 +21,26 @@ const createWagmiConfig_1 = require("../utils/createWagmiConfig");
21
21
  const useTWAuth_1 = require("./useTWAuth");
22
22
  const useUserQuery_1 = require("./useUserQuery");
23
23
  const debug = (0, debug_1.debugB3React)("useAuthentication");
24
- function useAuthentication(partnerId) {
24
+ function useAuthentication(partnerId, { skipAutoConnect = false } = {}) {
25
25
  const { onConnectCallback, onLogoutCallback } = (0, react_2.useContext)(LocalSDKProvider_1.LocalSDKContext);
26
26
  const { disconnect } = (0, react_3.useDisconnect)();
27
27
  const wallets = (0, react_3.useConnectedWallets)();
28
+ // Keep refs so logout() always disconnects current wallets, not stale closure values.
29
+ // autoConnectCore captures onConnect (and thus logout) from the first render before wallets
30
+ // are populated — without these refs, logout() would capture wallets=[] and disconnect nothing.
31
+ const walletsRef = (0, react_2.useRef)(wallets);
32
+ (0, react_2.useEffect)(() => {
33
+ walletsRef.current = wallets;
34
+ }, [wallets]);
28
35
  const activeWallet = (0, react_3.useActiveWallet)();
36
+ // Track the active wallet by ref so logout() can disconnect the exact reference
37
+ // stored in thirdweb's activeWalletStore. walletsRef.current (from useConnectedWallets)
38
+ // may hold a different object reference than what thirdweb considers "active",
39
+ // causing the identity check in onWalletDisconnect to fail silently.
40
+ const activeWalletRef = (0, react_2.useRef)(activeWallet);
41
+ (0, react_2.useEffect)(() => {
42
+ activeWalletRef.current = activeWallet;
43
+ }, [activeWallet]);
29
44
  const isAuthenticated = (0, react_1.useAuthStore)(state => state.isAuthenticated);
30
45
  const setIsAuthenticated = (0, react_1.useAuthStore)(state => state.setIsAuthenticated);
31
46
  const setIsConnected = (0, react_1.useAuthStore)(state => state.setIsConnected);
@@ -133,13 +148,26 @@ function useAuthentication(partnerId) {
133
148
  }, [activeWallet, partnerId, authenticate, setIsAuthenticated, setIsAuthenticating, setUser, setHasStartedConnecting]);
134
149
  const logout = (0, react_2.useCallback)(async (callback) => {
135
150
  // Only disconnect ecosystem/smart wallets, preserve EOA wallets (e.g. MetaMask)
136
- // so they remain available after re-login
137
- wallets.forEach(wallet => {
151
+ // so they remain available after re-login.
152
+ // Use walletsRef.current (not the stale closure value) so we always get current wallets —
153
+ // autoConnectCore captures logout from the first render when wallets is still [].
154
+ walletsRef.current.forEach(wallet => {
138
155
  debug("@@logout:wallet", wallet.id);
139
156
  if (wallet.id.startsWith("ecosystem.") || wallet.id === "smart") {
140
157
  disconnect(wallet);
141
158
  }
142
159
  });
160
+ // Also disconnect the active wallet using the exact reference from thirdweb's
161
+ // activeWalletStore. The wallets in walletsRef (from useConnectedWallets) may be
162
+ // different object references than what thirdweb holds as "active". Thirdweb's
163
+ // onWalletDisconnect uses strict identity (===) to decide whether to clear
164
+ // activeAccountStore — if the reference doesn't match, activeAccount stays set
165
+ // and ConnectEmbed renders show=false (blank).
166
+ if (activeWalletRef.current &&
167
+ (activeWalletRef.current.id.startsWith("ecosystem.") || activeWalletRef.current.id === "smart")) {
168
+ debug("@@logout:disconnecting active wallet", activeWalletRef.current.id);
169
+ disconnect(activeWalletRef.current);
170
+ }
143
171
  // Clear user-specific storage but preserve wallet connection state
144
172
  // so EOA wallets (e.g. MetaMask) can auto-reconnect on next login
145
173
  if (typeof localStorage !== "undefined") {
@@ -150,12 +178,19 @@ function useAuthentication(partnerId) {
150
178
  debug("@@logout:loggedOut");
151
179
  setIsAuthenticated(false);
152
180
  setIsConnected(false);
181
+ // Reset isAuthenticating so any in-flight page-load auto-connect that set it true
182
+ // does not keep the login modal spinner stuck after logout() is called.
183
+ setIsAuthenticating(false);
153
184
  setUser();
154
185
  callback?.();
155
186
  if (onLogoutCallback) {
156
187
  await onLogoutCallback();
157
188
  }
158
- }, [disconnect, wallets, setIsAuthenticated, setUser, setIsConnected, onLogoutCallback]);
189
+ },
190
+ // wallets intentionally omitted — we use walletsRef.current so this callback stays stable
191
+ // and always operates on current wallets even when captured in stale closures.
192
+ // eslint-disable-next-line react-hooks/exhaustive-deps
193
+ [disconnect, setIsAuthenticated, setIsAuthenticating, setUser, setIsConnected, onLogoutCallback]);
159
194
  const onConnect = (0, react_2.useCallback)(async (_walleAutoConnectedWith, allConnectedWallets) => {
160
195
  debug("@@useAuthentication:onConnect", { _walleAutoConnectedWith, allConnectedWallets });
161
196
  try {
@@ -203,23 +238,32 @@ function useAuthentication(partnerId) {
203
238
  ]);
204
239
  const { isLoading: useAutoConnectLoading } = (0, react_3.useAutoConnect)({
205
240
  client: thirdweb_1.client,
206
- wallets: [wallet],
241
+ // When skipAutoConnect is true (e.g. LoginStepContent, SignInWithB3Flow), pass an empty
242
+ // wallets array so useAutoConnect completes immediately without firing onConnect.
243
+ // Only AuthenticationProvider (the primary instance) should own auto-connect.
244
+ wallets: skipAutoConnect ? [] : [wallet],
207
245
  onConnect,
208
246
  onTimeout: () => {
247
+ if (skipAutoConnect)
248
+ return;
209
249
  logout().catch(error => {
210
250
  debug("@@useAuthentication:logout on timeout failed", { error });
211
251
  });
212
252
  },
213
253
  });
214
254
  /**
215
- * useAutoConnectLoading starts as false
255
+ * useAutoConnectLoading starts as false.
256
+ * Only the primary (non-skip) instance manages isAuthenticating via this effect
257
+ * to avoid race conditions when multiple useAuthentication instances are mounted.
216
258
  */
217
259
  (0, react_2.useEffect)(() => {
260
+ if (skipAutoConnect)
261
+ return;
218
262
  if (!useAutoConnectLoading && useAutoConnectLoadingPrevious.current && !hasStartedConnecting) {
219
263
  setIsAuthenticating(false);
220
264
  }
221
265
  useAutoConnectLoadingPrevious.current = useAutoConnectLoading;
222
- }, [useAutoConnectLoading, hasStartedConnecting, setIsAuthenticating]);
266
+ }, [useAutoConnectLoading, hasStartedConnecting, setIsAuthenticating, skipAutoConnect]);
223
267
  const isReady = isAuthenticated && !isAuthenticating;
224
268
  return {
225
269
  logout,
@@ -68,7 +68,8 @@ function useGetAllTWSigners({ chain, accountAddress, queryOptions }) {
68
68
  });
69
69
  return result;
70
70
  },
71
- enabled: Boolean(chain && accountAddress),
71
+ // Respect queryOptions.enabled if explicitly set (e.g. signersEnabled=false from SignInWithB3Flow)
72
+ enabled: queryOptions?.enabled !== false && Boolean(chain && accountAddress),
72
73
  refetchOnMount: true,
73
74
  refetchOnWindowFocus: true,
74
75
  refetchOnReconnect: true,
@@ -13,7 +13,7 @@ export function SignIn(props) {
13
13
  const { address: globalAddress, ensName, connectedSmartWallet, connectedEOAWallet, isActiveSmartWallet, isActiveEOAWallet, smartWalletIcon, } = useAccountWallet();
14
14
  const { data: walletImage } = useWalletImage(connectedEOAWallet?.id);
15
15
  const isMobile = useIsMobile();
16
- const { logout } = useAuthentication(partnerId);
16
+ const { logout } = useAuthentication(partnerId, { skipAutoConnect: true });
17
17
  const onDisconnect = async () => {
18
18
  await logout();
19
19
  };
@@ -14,16 +14,23 @@ const MAX_REFETCH_ATTEMPTS = 20;
14
14
  */
15
15
  export function SignInWithB3Flow({ strategies, onLoginSuccess, onSessionKeySuccess, onError, chain, sessionKeyAddress, partnerId, closeAfterLogin = false, source = "signInWithB3Button", signersEnabled = false, }) {
16
16
  const { automaticallySetFirstEoa } = useB3Config();
17
- const { user, logout } = useAuthentication(partnerId);
18
- // FIXME Logout before login to ensure a clean state
17
+ // skipAutoConnect: this component intentionally logs out on mount to show a fresh login screen.
18
+ // AuthenticationProvider is the sole owner of useAutoConnect to avoid competing auth cycles.
19
+ const { user, logout } = useAuthentication(partnerId, { skipAutoConnect: true });
20
+ // Tracks whether the pre-login logout has finished.
21
+ // We must not render ConnectEmbed until logout (wallet disconnect) completes,
22
+ // otherwise the wallet state disrupts ConnectEmbed causing a blank modal.
23
+ const [readyToShowLogin, setReadyToShowLogin] = useState(source === "requestPermissions");
19
24
  const hasLoggedOutRef = useRef(false);
20
25
  useEffect(() => {
21
26
  if (hasLoggedOutRef.current)
22
27
  return;
23
28
  if (source !== "requestPermissions") {
24
29
  debug("Logging out before login");
25
- logout();
26
30
  hasLoggedOutRef.current = true;
31
+ logout().finally(() => {
32
+ setReadyToShowLogin(true);
33
+ });
27
34
  }
28
35
  }, [source, logout]);
29
36
  const [step, setStep] = useState(source === "requestPermissions" ? null : "login");
@@ -228,8 +235,9 @@ export function SignInWithB3Flow({ strategies, onLoginSuccess, onSessionKeySucce
228
235
  content = (_jsx(LoginStepContainer, { partnerId: partnerId, children: _jsx("div", { className: "p-4 text-center text-red-500", children: refetchError }) }));
229
236
  }
230
237
  else if (step === "login") {
231
- // Show loading spinner
232
- if (isAuthenticating || (isFetchingSigners && step === "login") || source === "requestPermissions") {
238
+ // Show loading spinner while: authenticating, waiting for pre-login logout to finish,
239
+ // or fetching signers.
240
+ if (!readyToShowLogin || isAuthenticating || isFetchingSigners) {
233
241
  content = (_jsx(LoginStepContainer, { partnerId: partnerId, children: _jsx("div", { className: "my-8 flex min-h-[350px] items-center justify-center", children: _jsx(Loading, { variant: "white", size: "lg" }) }) }));
234
242
  }
235
243
  else {
@@ -8,7 +8,7 @@ export function SignInWithB3Privy({ onSuccess, onError, chain }) {
8
8
  const { isLoading, connectTw, fullToken } = useHandleConnectWithPrivy(chain, onSuccess);
9
9
  const setIsAuthenticating = useAuthStore(state => state.setIsAuthenticating);
10
10
  const setIsAuthenticated = useAuthStore(state => state.setIsAuthenticated);
11
- const { logout } = useAuthentication(partnerId);
11
+ const { logout } = useAuthentication(partnerId, { skipAutoConnect: true });
12
12
  debug("@@SignInWithB3Privy", {
13
13
  isLoading,
14
14
  fullToken,
@@ -19,5 +19,5 @@ interface LoginStepContainerProps {
19
19
  partnerId?: string;
20
20
  }
21
21
  export declare function LoginStepContainer({ children, partnerId }: LoginStepContainerProps): import("react/jsx-runtime").JSX.Element;
22
- export declare function LoginStep({ onSuccess, chain }: LoginStepProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function LoginStep({ onSuccess, chain }: LoginStepProps): import("react/jsx-runtime").JSX.Element | null;
23
23
  export {};
@@ -3,6 +3,7 @@ import { useAuthentication, useB3Config, useQueryB3 } from "../../../../../globa
3
3
  import { ecosystemWalletId } from "../../../../../shared/constants/index.js";
4
4
  import { client } from "../../../../../shared/utils/thirdweb.js";
5
5
  import { ConnectEmbed, darkTheme, lightTheme } from "thirdweb/react";
6
+ import { useMemo } from "react";
6
7
  import { ecosystemWallet } from "thirdweb/wallets";
7
8
  export function LoginStepContainer({ children, partnerId }) {
8
9
  const { data: partner } = useQueryB3("global-accounts-partners", "find", {
@@ -14,30 +15,17 @@ export function LoginStepContainer({ children, partnerId }) {
14
15
  const partnerLogo = partner?.data?.[0]?.loginCustomization?.logoUrl;
15
16
  return (_jsxs("div", { className: "bg-b3-react-background flex flex-col items-center justify-center pt-6", children: [partnerLogo && (_jsx("img", { src: partnerLogo, alt: "Partner Logo", className: "partner-logo mb-6 h-12 w-auto object-contain" })), children] }));
16
17
  }
17
- export function LoginStep({ onSuccess, chain }) {
18
- const { partnerId, theme } = useB3Config();
19
- const wallet = ecosystemWallet(ecosystemWalletId, {
20
- partnerId: partnerId,
21
- });
22
- const { onConnect } = useAuthentication(partnerId);
23
- return (_jsx(LoginStepContainer, { partnerId: partnerId, children: _jsx(ConnectEmbed, { showThirdwebBranding: false, client: client, chain: chain, wallets: [wallet], theme: theme === "light"
24
- ? lightTheme({
25
- colors: {
26
- modalBg: "hsl(var(--b3-react-background))",
27
- },
28
- })
29
- : darkTheme({
30
- colors: {
31
- modalBg: "hsl(var(--b3-react-background))",
32
- },
33
- }), style: {
34
- width: "100%",
35
- height: "100%",
36
- border: 0,
37
- }, header: {
38
- title: "Sign in with B3",
39
- titleIcon: "https://cdn.b3.fun/b3_logo.svg",
40
- }, className: "b3-login-step", onConnect: async (wallet, allConnectedWallets) => {
18
+ /** Inner component that only mounts when partnerId is a non-empty string.
19
+ * Keeps all hooks unconditional without calling useAuthentication(""). */
20
+ function LoginStepContent({ onSuccess, chain, partnerId, theme, }) {
21
+ const wallet = useMemo(() => ecosystemWallet(ecosystemWalletId, { partnerId }), [partnerId]);
22
+ // skipAutoConnect: AuthenticationProvider already owns the auto-connect instance.
23
+ // Creating another here would cause a second authentication cycle (another 401 attempt)
24
+ // that makes the modal flash between spinner and blank before finally showing the login form.
25
+ const { onConnect } = useAuthentication(partnerId, { skipAutoConnect: true });
26
+ return (_jsx(LoginStepContainer, { partnerId: partnerId, children: _jsx(ConnectEmbed, { showThirdwebBranding: false, autoConnect: false, client: client, chain: chain, wallets: [wallet], theme: theme === "light"
27
+ ? lightTheme({ colors: { modalBg: "hsl(var(--b3-react-background))" } })
28
+ : darkTheme({ colors: { modalBg: "hsl(var(--b3-react-background))" } }), style: { width: "100%", height: "100%", border: 0 }, header: { title: "Sign in with B3", titleIcon: "https://cdn.b3.fun/b3_logo.svg" }, className: "b3-login-step", onConnect: async (wallet, allConnectedWallets) => {
41
29
  await onConnect(wallet, allConnectedWallets);
42
30
  const account = wallet.getAccount();
43
31
  if (!account)
@@ -45,3 +33,12 @@ export function LoginStep({ onSuccess, chain }) {
45
33
  await onSuccess(account);
46
34
  } }) }));
47
35
  }
36
+ export function LoginStep({ onSuccess, chain }) {
37
+ const { partnerId, theme } = useB3Config();
38
+ // partnerId may be undefined during the brief B3Provider hydration window.
39
+ // Return null rather than rendering ConnectEmbed with an invalid ecosystem
40
+ // wallet config (which causes a blank screen).
41
+ if (!partnerId)
42
+ return null;
43
+ return _jsx(LoginStepContent, { onSuccess: onSuccess, chain: chain, partnerId: partnerId, theme: theme });
44
+ }
@@ -13,7 +13,7 @@ export function LoginStepCustom({ onSuccess, onError, chain, strategies, maxInit
13
13
  const { connect } = useConnect(partnerId, chain);
14
14
  const setIsAuthenticating = useAuthStore(state => state.setIsAuthenticating);
15
15
  const setIsAuthenticated = useAuthStore(state => state.setIsAuthenticated);
16
- const { logout } = useAuthentication(partnerId);
16
+ const { logout } = useAuthentication(partnerId, { skipAutoConnect: true });
17
17
  const { connect: connectTW } = useConnectTW();
18
18
  // Split strategies into auth and wallet types
19
19
  const authStrategies = strategies.filter(s => !isWalletType(s));
@@ -1,6 +1,8 @@
1
1
  import { Wallet } from "thirdweb/wallets";
2
2
  import { preAuthenticate } from "thirdweb/wallets/in-app";
3
- export declare function useAuthentication(partnerId: string): {
3
+ export declare function useAuthentication(partnerId: string, { skipAutoConnect }?: {
4
+ skipAutoConnect?: boolean;
5
+ }): {
4
6
  logout: (callback?: () => void) => Promise<void>;
5
7
  isAuthenticated: boolean;
6
8
  isReady: boolean;
@@ -15,11 +15,26 @@ import { createWagmiConfig } from "../utils/createWagmiConfig.js";
15
15
  import { useTWAuth } from "./useTWAuth.js";
16
16
  import { useUserQuery } from "./useUserQuery.js";
17
17
  const debug = debugB3React("useAuthentication");
18
- export function useAuthentication(partnerId) {
18
+ export function useAuthentication(partnerId, { skipAutoConnect = false } = {}) {
19
19
  const { onConnectCallback, onLogoutCallback } = useContext(LocalSDKContext);
20
20
  const { disconnect } = useDisconnect();
21
21
  const wallets = useConnectedWallets();
22
+ // Keep refs so logout() always disconnects current wallets, not stale closure values.
23
+ // autoConnectCore captures onConnect (and thus logout) from the first render before wallets
24
+ // are populated — without these refs, logout() would capture wallets=[] and disconnect nothing.
25
+ const walletsRef = useRef(wallets);
26
+ useEffect(() => {
27
+ walletsRef.current = wallets;
28
+ }, [wallets]);
22
29
  const activeWallet = useActiveWallet();
30
+ // Track the active wallet by ref so logout() can disconnect the exact reference
31
+ // stored in thirdweb's activeWalletStore. walletsRef.current (from useConnectedWallets)
32
+ // may hold a different object reference than what thirdweb considers "active",
33
+ // causing the identity check in onWalletDisconnect to fail silently.
34
+ const activeWalletRef = useRef(activeWallet);
35
+ useEffect(() => {
36
+ activeWalletRef.current = activeWallet;
37
+ }, [activeWallet]);
23
38
  const isAuthenticated = useAuthStore(state => state.isAuthenticated);
24
39
  const setIsAuthenticated = useAuthStore(state => state.setIsAuthenticated);
25
40
  const setIsConnected = useAuthStore(state => state.setIsConnected);
@@ -127,13 +142,26 @@ export function useAuthentication(partnerId) {
127
142
  }, [activeWallet, partnerId, authenticate, setIsAuthenticated, setIsAuthenticating, setUser, setHasStartedConnecting]);
128
143
  const logout = useCallback(async (callback) => {
129
144
  // Only disconnect ecosystem/smart wallets, preserve EOA wallets (e.g. MetaMask)
130
- // so they remain available after re-login
131
- wallets.forEach(wallet => {
145
+ // so they remain available after re-login.
146
+ // Use walletsRef.current (not the stale closure value) so we always get current wallets —
147
+ // autoConnectCore captures logout from the first render when wallets is still [].
148
+ walletsRef.current.forEach(wallet => {
132
149
  debug("@@logout:wallet", wallet.id);
133
150
  if (wallet.id.startsWith("ecosystem.") || wallet.id === "smart") {
134
151
  disconnect(wallet);
135
152
  }
136
153
  });
154
+ // Also disconnect the active wallet using the exact reference from thirdweb's
155
+ // activeWalletStore. The wallets in walletsRef (from useConnectedWallets) may be
156
+ // different object references than what thirdweb holds as "active". Thirdweb's
157
+ // onWalletDisconnect uses strict identity (===) to decide whether to clear
158
+ // activeAccountStore — if the reference doesn't match, activeAccount stays set
159
+ // and ConnectEmbed renders show=false (blank).
160
+ if (activeWalletRef.current &&
161
+ (activeWalletRef.current.id.startsWith("ecosystem.") || activeWalletRef.current.id === "smart")) {
162
+ debug("@@logout:disconnecting active wallet", activeWalletRef.current.id);
163
+ disconnect(activeWalletRef.current);
164
+ }
137
165
  // Clear user-specific storage but preserve wallet connection state
138
166
  // so EOA wallets (e.g. MetaMask) can auto-reconnect on next login
139
167
  if (typeof localStorage !== "undefined") {
@@ -144,12 +172,19 @@ export function useAuthentication(partnerId) {
144
172
  debug("@@logout:loggedOut");
145
173
  setIsAuthenticated(false);
146
174
  setIsConnected(false);
175
+ // Reset isAuthenticating so any in-flight page-load auto-connect that set it true
176
+ // does not keep the login modal spinner stuck after logout() is called.
177
+ setIsAuthenticating(false);
147
178
  setUser();
148
179
  callback?.();
149
180
  if (onLogoutCallback) {
150
181
  await onLogoutCallback();
151
182
  }
152
- }, [disconnect, wallets, setIsAuthenticated, setUser, setIsConnected, onLogoutCallback]);
183
+ },
184
+ // wallets intentionally omitted — we use walletsRef.current so this callback stays stable
185
+ // and always operates on current wallets even when captured in stale closures.
186
+ // eslint-disable-next-line react-hooks/exhaustive-deps
187
+ [disconnect, setIsAuthenticated, setIsAuthenticating, setUser, setIsConnected, onLogoutCallback]);
153
188
  const onConnect = useCallback(async (_walleAutoConnectedWith, allConnectedWallets) => {
154
189
  debug("@@useAuthentication:onConnect", { _walleAutoConnectedWith, allConnectedWallets });
155
190
  try {
@@ -197,23 +232,32 @@ export function useAuthentication(partnerId) {
197
232
  ]);
198
233
  const { isLoading: useAutoConnectLoading } = useAutoConnect({
199
234
  client,
200
- wallets: [wallet],
235
+ // When skipAutoConnect is true (e.g. LoginStepContent, SignInWithB3Flow), pass an empty
236
+ // wallets array so useAutoConnect completes immediately without firing onConnect.
237
+ // Only AuthenticationProvider (the primary instance) should own auto-connect.
238
+ wallets: skipAutoConnect ? [] : [wallet],
201
239
  onConnect,
202
240
  onTimeout: () => {
241
+ if (skipAutoConnect)
242
+ return;
203
243
  logout().catch(error => {
204
244
  debug("@@useAuthentication:logout on timeout failed", { error });
205
245
  });
206
246
  },
207
247
  });
208
248
  /**
209
- * useAutoConnectLoading starts as false
249
+ * useAutoConnectLoading starts as false.
250
+ * Only the primary (non-skip) instance manages isAuthenticating via this effect
251
+ * to avoid race conditions when multiple useAuthentication instances are mounted.
210
252
  */
211
253
  useEffect(() => {
254
+ if (skipAutoConnect)
255
+ return;
212
256
  if (!useAutoConnectLoading && useAutoConnectLoadingPrevious.current && !hasStartedConnecting) {
213
257
  setIsAuthenticating(false);
214
258
  }
215
259
  useAutoConnectLoadingPrevious.current = useAutoConnectLoading;
216
- }, [useAutoConnectLoading, hasStartedConnecting, setIsAuthenticating]);
260
+ }, [useAutoConnectLoading, hasStartedConnecting, setIsAuthenticating, skipAutoConnect]);
217
261
  const isReady = isAuthenticated && !isAuthenticating;
218
262
  return {
219
263
  logout,
@@ -62,7 +62,8 @@ export function useGetAllTWSigners({ chain, accountAddress, queryOptions }) {
62
62
  });
63
63
  return result;
64
64
  },
65
- enabled: Boolean(chain && accountAddress),
65
+ // Respect queryOptions.enabled if explicitly set (e.g. signersEnabled=false from SignInWithB3Flow)
66
+ enabled: queryOptions?.enabled !== false && Boolean(chain && accountAddress),
66
67
  refetchOnMount: true,
67
68
  refetchOnWindowFocus: true,
68
69
  refetchOnReconnect: true,
@@ -19,5 +19,5 @@ interface LoginStepContainerProps {
19
19
  partnerId?: string;
20
20
  }
21
21
  export declare function LoginStepContainer({ children, partnerId }: LoginStepContainerProps): import("react/jsx-runtime").JSX.Element;
22
- export declare function LoginStep({ onSuccess, chain }: LoginStepProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function LoginStep({ onSuccess, chain }: LoginStepProps): import("react/jsx-runtime").JSX.Element | null;
23
23
  export {};
@@ -1,6 +1,8 @@
1
1
  import { Wallet } from "thirdweb/wallets";
2
2
  import { preAuthenticate } from "thirdweb/wallets/in-app";
3
- export declare function useAuthentication(partnerId: string): {
3
+ export declare function useAuthentication(partnerId: string, { skipAutoConnect }?: {
4
+ skipAutoConnect?: boolean;
5
+ }): {
4
6
  logout: (callback?: () => void) => Promise<void>;
5
7
  isAuthenticated: boolean;
6
8
  isReady: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.1.68-alpha.8",
3
+ "version": "0.1.68-alpha.9",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -44,7 +44,7 @@ export function SignIn(props: SignInWithB3Props) {
44
44
  const { data: walletImage } = useWalletImage(connectedEOAWallet?.id);
45
45
 
46
46
  const isMobile = useIsMobile();
47
- const { logout } = useAuthentication(partnerId);
47
+ const { logout } = useAuthentication(partnerId, { skipAutoConnect: true });
48
48
  const onDisconnect = async (): Promise<void> => {
49
49
  await logout();
50
50
  };
@@ -35,16 +35,23 @@ export function SignInWithB3Flow({
35
35
  signersEnabled = false,
36
36
  }: SignInWithB3ModalProps) {
37
37
  const { automaticallySetFirstEoa } = useB3Config();
38
- const { user, logout } = useAuthentication(partnerId);
38
+ // skipAutoConnect: this component intentionally logs out on mount to show a fresh login screen.
39
+ // AuthenticationProvider is the sole owner of useAutoConnect to avoid competing auth cycles.
40
+ const { user, logout } = useAuthentication(partnerId, { skipAutoConnect: true });
39
41
 
40
- // FIXME Logout before login to ensure a clean state
42
+ // Tracks whether the pre-login logout has finished.
43
+ // We must not render ConnectEmbed until logout (wallet disconnect) completes,
44
+ // otherwise the wallet state disrupts ConnectEmbed causing a blank modal.
45
+ const [readyToShowLogin, setReadyToShowLogin] = useState(source === "requestPermissions");
41
46
  const hasLoggedOutRef = useRef(false);
42
47
  useEffect(() => {
43
48
  if (hasLoggedOutRef.current) return;
44
49
  if (source !== "requestPermissions") {
45
50
  debug("Logging out before login");
46
- logout();
47
51
  hasLoggedOutRef.current = true;
52
+ logout().finally(() => {
53
+ setReadyToShowLogin(true);
54
+ });
48
55
  }
49
56
  }, [source, logout]);
50
57
 
@@ -275,8 +282,9 @@ export function SignInWithB3Flow({
275
282
  </LoginStepContainer>
276
283
  );
277
284
  } else if (step === "login") {
278
- // Show loading spinner
279
- if (isAuthenticating || (isFetchingSigners && step === "login") || source === "requestPermissions") {
285
+ // Show loading spinner while: authenticating, waiting for pre-login logout to finish,
286
+ // or fetching signers.
287
+ if (!readyToShowLogin || isAuthenticating || isFetchingSigners) {
280
288
  content = (
281
289
  <LoginStepContainer partnerId={partnerId}>
282
290
  <div className="my-8 flex min-h-[350px] items-center justify-center">
@@ -23,7 +23,7 @@ export function SignInWithB3Privy({ onSuccess, onError, chain }: SignInWithB3Pri
23
23
  const { isLoading, connectTw, fullToken } = useHandleConnectWithPrivy(chain, onSuccess);
24
24
  const setIsAuthenticating = useAuthStore(state => state.setIsAuthenticating);
25
25
  const setIsAuthenticated = useAuthStore(state => state.setIsAuthenticated);
26
- const { logout } = useAuthentication(partnerId);
26
+ const { logout } = useAuthentication(partnerId, { skipAutoConnect: true });
27
27
 
28
28
  debug("@@SignInWithB3Privy", {
29
29
  isLoading,
@@ -3,6 +3,7 @@ import { ecosystemWalletId } from "@b3dotfun/sdk/shared/constants";
3
3
  import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
4
4
  import { Chain } from "thirdweb";
5
5
  import { ConnectEmbed, darkTheme, lightTheme } from "thirdweb/react";
6
+ import { useMemo } from "react";
6
7
  import { Account, ecosystemWallet, SingleStepAuthArgsType } from "thirdweb/wallets";
7
8
  /**
8
9
  * Props for the LoginStep component
@@ -49,42 +50,40 @@ export function LoginStepContainer({ children, partnerId }: LoginStepContainerPr
49
50
  );
50
51
  }
51
52
 
52
- export function LoginStep({ onSuccess, chain }: LoginStepProps) {
53
- const { partnerId, theme } = useB3Config();
54
- const wallet = ecosystemWallet(ecosystemWalletId, {
55
- partnerId: partnerId,
56
- });
57
- const { onConnect } = useAuthentication(partnerId);
53
+ /** Inner component that only mounts when partnerId is a non-empty string.
54
+ * Keeps all hooks unconditional without calling useAuthentication(""). */
55
+ function LoginStepContent({
56
+ onSuccess,
57
+ chain,
58
+ partnerId,
59
+ theme,
60
+ }: {
61
+ onSuccess: (account: Account) => Promise<void>;
62
+ chain: Chain;
63
+ partnerId: string;
64
+ theme: string;
65
+ }) {
66
+ const wallet = useMemo(() => ecosystemWallet(ecosystemWalletId, { partnerId }), [partnerId]);
67
+ // skipAutoConnect: AuthenticationProvider already owns the auto-connect instance.
68
+ // Creating another here would cause a second authentication cycle (another 401 attempt)
69
+ // that makes the modal flash between spinner and blank before finally showing the login form.
70
+ const { onConnect } = useAuthentication(partnerId, { skipAutoConnect: true });
58
71
 
59
72
  return (
60
73
  <LoginStepContainer partnerId={partnerId}>
61
74
  <ConnectEmbed
62
75
  showThirdwebBranding={false}
76
+ autoConnect={false}
63
77
  client={client}
64
78
  chain={chain}
65
79
  wallets={[wallet]}
66
80
  theme={
67
81
  theme === "light"
68
- ? lightTheme({
69
- colors: {
70
- modalBg: "hsl(var(--b3-react-background))",
71
- },
72
- })
73
- : darkTheme({
74
- colors: {
75
- modalBg: "hsl(var(--b3-react-background))",
76
- },
77
- })
82
+ ? lightTheme({ colors: { modalBg: "hsl(var(--b3-react-background))" } })
83
+ : darkTheme({ colors: { modalBg: "hsl(var(--b3-react-background))" } })
78
84
  }
79
- style={{
80
- width: "100%",
81
- height: "100%",
82
- border: 0,
83
- }}
84
- header={{
85
- title: "Sign in with B3",
86
- titleIcon: "https://cdn.b3.fun/b3_logo.svg",
87
- }}
85
+ style={{ width: "100%", height: "100%", border: 0 }}
86
+ header={{ title: "Sign in with B3", titleIcon: "https://cdn.b3.fun/b3_logo.svg" }}
88
87
  className="b3-login-step"
89
88
  onConnect={async (wallet, allConnectedWallets) => {
90
89
  await onConnect(wallet, allConnectedWallets);
@@ -96,3 +95,14 @@ export function LoginStep({ onSuccess, chain }: LoginStepProps) {
96
95
  </LoginStepContainer>
97
96
  );
98
97
  }
98
+
99
+ export function LoginStep({ onSuccess, chain }: LoginStepProps) {
100
+ const { partnerId, theme } = useB3Config();
101
+
102
+ // partnerId may be undefined during the brief B3Provider hydration window.
103
+ // Return null rather than rendering ConnectEmbed with an invalid ecosystem
104
+ // wallet config (which causes a blank screen).
105
+ if (!partnerId) return null;
106
+
107
+ return <LoginStepContent onSuccess={onSuccess} chain={chain} partnerId={partnerId} theme={theme} />;
108
+ }
@@ -43,7 +43,7 @@ export function LoginStepCustom({
43
43
  const { connect } = useConnect(partnerId, chain);
44
44
  const setIsAuthenticating = useAuthStore(state => state.setIsAuthenticating);
45
45
  const setIsAuthenticated = useAuthStore(state => state.setIsAuthenticated);
46
- const { logout } = useAuthentication(partnerId);
46
+ const { logout } = useAuthentication(partnerId, { skipAutoConnect: true });
47
47
  const { connect: connectTW } = useConnectTW();
48
48
 
49
49
  // Split strategies into auth and wallet types
@@ -24,11 +24,26 @@ import { useUserQuery } from "./useUserQuery";
24
24
 
25
25
  const debug = debugB3React("useAuthentication");
26
26
 
27
- export function useAuthentication(partnerId: string) {
27
+ export function useAuthentication(partnerId: string, { skipAutoConnect = false }: { skipAutoConnect?: boolean } = {}) {
28
28
  const { onConnectCallback, onLogoutCallback } = useContext(LocalSDKContext);
29
29
  const { disconnect } = useDisconnect();
30
30
  const wallets = useConnectedWallets();
31
+ // Keep refs so logout() always disconnects current wallets, not stale closure values.
32
+ // autoConnectCore captures onConnect (and thus logout) from the first render before wallets
33
+ // are populated — without these refs, logout() would capture wallets=[] and disconnect nothing.
34
+ const walletsRef = useRef(wallets);
35
+ useEffect(() => {
36
+ walletsRef.current = wallets;
37
+ }, [wallets]);
31
38
  const activeWallet = useActiveWallet();
39
+ // Track the active wallet by ref so logout() can disconnect the exact reference
40
+ // stored in thirdweb's activeWalletStore. walletsRef.current (from useConnectedWallets)
41
+ // may hold a different object reference than what thirdweb considers "active",
42
+ // causing the identity check in onWalletDisconnect to fail silently.
43
+ const activeWalletRef = useRef(activeWallet);
44
+ useEffect(() => {
45
+ activeWalletRef.current = activeWallet;
46
+ }, [activeWallet]);
32
47
  const isAuthenticated = useAuthStore(state => state.isAuthenticated);
33
48
  const setIsAuthenticated = useAuthStore(state => state.setIsAuthenticated);
34
49
  const setIsConnected = useAuthStore(state => state.setIsConnected);
@@ -154,14 +169,30 @@ export function useAuthentication(partnerId: string) {
154
169
  const logout = useCallback(
155
170
  async (callback?: () => void) => {
156
171
  // Only disconnect ecosystem/smart wallets, preserve EOA wallets (e.g. MetaMask)
157
- // so they remain available after re-login
158
- wallets.forEach(wallet => {
172
+ // so they remain available after re-login.
173
+ // Use walletsRef.current (not the stale closure value) so we always get current wallets —
174
+ // autoConnectCore captures logout from the first render when wallets is still [].
175
+ walletsRef.current.forEach(wallet => {
159
176
  debug("@@logout:wallet", wallet.id);
160
177
  if (wallet.id.startsWith("ecosystem.") || wallet.id === "smart") {
161
178
  disconnect(wallet);
162
179
  }
163
180
  });
164
181
 
182
+ // Also disconnect the active wallet using the exact reference from thirdweb's
183
+ // activeWalletStore. The wallets in walletsRef (from useConnectedWallets) may be
184
+ // different object references than what thirdweb holds as "active". Thirdweb's
185
+ // onWalletDisconnect uses strict identity (===) to decide whether to clear
186
+ // activeAccountStore — if the reference doesn't match, activeAccount stays set
187
+ // and ConnectEmbed renders show=false (blank).
188
+ if (
189
+ activeWalletRef.current &&
190
+ (activeWalletRef.current.id.startsWith("ecosystem.") || activeWalletRef.current.id === "smart")
191
+ ) {
192
+ debug("@@logout:disconnecting active wallet", activeWalletRef.current.id);
193
+ disconnect(activeWalletRef.current);
194
+ }
195
+
165
196
  // Clear user-specific storage but preserve wallet connection state
166
197
  // so EOA wallets (e.g. MetaMask) can auto-reconnect on next login
167
198
  if (typeof localStorage !== "undefined") {
@@ -174,6 +205,9 @@ export function useAuthentication(partnerId: string) {
174
205
 
175
206
  setIsAuthenticated(false);
176
207
  setIsConnected(false);
208
+ // Reset isAuthenticating so any in-flight page-load auto-connect that set it true
209
+ // does not keep the login modal spinner stuck after logout() is called.
210
+ setIsAuthenticating(false);
177
211
  setUser();
178
212
  callback?.();
179
213
 
@@ -181,7 +215,10 @@ export function useAuthentication(partnerId: string) {
181
215
  await onLogoutCallback();
182
216
  }
183
217
  },
184
- [disconnect, wallets, setIsAuthenticated, setUser, setIsConnected, onLogoutCallback],
218
+ // wallets intentionally omitted we use walletsRef.current so this callback stays stable
219
+ // and always operates on current wallets even when captured in stale closures.
220
+ // eslint-disable-next-line react-hooks/exhaustive-deps
221
+ [disconnect, setIsAuthenticated, setIsAuthenticating, setUser, setIsConnected, onLogoutCallback],
185
222
  );
186
223
 
187
224
  const onConnect = useCallback(
@@ -208,7 +245,6 @@ export function useAuthentication(partnerId: string) {
208
245
  debug("@@useAuthentication:onConnect:failed", { error });
209
246
  setIsAuthenticated(false);
210
247
  setUser(undefined);
211
-
212
248
  await logout();
213
249
  } finally {
214
250
  setIsAuthenticating(false);
@@ -238,9 +274,13 @@ export function useAuthentication(partnerId: string) {
238
274
 
239
275
  const { isLoading: useAutoConnectLoading } = useAutoConnect({
240
276
  client,
241
- wallets: [wallet],
277
+ // When skipAutoConnect is true (e.g. LoginStepContent, SignInWithB3Flow), pass an empty
278
+ // wallets array so useAutoConnect completes immediately without firing onConnect.
279
+ // Only AuthenticationProvider (the primary instance) should own auto-connect.
280
+ wallets: skipAutoConnect ? [] : [wallet],
242
281
  onConnect,
243
282
  onTimeout: () => {
283
+ if (skipAutoConnect) return;
244
284
  logout().catch(error => {
245
285
  debug("@@useAuthentication:logout on timeout failed", { error });
246
286
  });
@@ -248,14 +288,17 @@ export function useAuthentication(partnerId: string) {
248
288
  });
249
289
 
250
290
  /**
251
- * useAutoConnectLoading starts as false
291
+ * useAutoConnectLoading starts as false.
292
+ * Only the primary (non-skip) instance manages isAuthenticating via this effect
293
+ * to avoid race conditions when multiple useAuthentication instances are mounted.
252
294
  */
253
295
  useEffect(() => {
296
+ if (skipAutoConnect) return;
254
297
  if (!useAutoConnectLoading && useAutoConnectLoadingPrevious.current && !hasStartedConnecting) {
255
298
  setIsAuthenticating(false);
256
299
  }
257
300
  useAutoConnectLoadingPrevious.current = useAutoConnectLoading;
258
- }, [useAutoConnectLoading, hasStartedConnecting, setIsAuthenticating]);
301
+ }, [useAutoConnectLoading, hasStartedConnecting, setIsAuthenticating, skipAutoConnect]);
259
302
 
260
303
  const isReady = isAuthenticated && !isAuthenticating;
261
304
 
@@ -130,7 +130,8 @@ export function useGetAllTWSigners({ chain, accountAddress, queryOptions }: UseG
130
130
  });
131
131
  return result;
132
132
  },
133
- enabled: Boolean(chain && accountAddress),
133
+ // Respect queryOptions.enabled if explicitly set (e.g. signersEnabled=false from SignInWithB3Flow)
134
+ enabled: queryOptions?.enabled !== false && Boolean(chain && accountAddress),
134
135
  refetchOnMount: true,
135
136
  refetchOnWindowFocus: true,
136
137
  refetchOnReconnect: true,