@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.
- package/dist/cjs/global-account/react/components/SignInWithB3/SignIn.js +1 -1
- package/dist/cjs/global-account/react/components/SignInWithB3/SignInWithB3Flow.js +13 -5
- package/dist/cjs/global-account/react/components/SignInWithB3/SignInWithB3Privy.js +1 -1
- package/dist/cjs/global-account/react/components/SignInWithB3/steps/LoginStep.d.ts +1 -1
- package/dist/cjs/global-account/react/components/SignInWithB3/steps/LoginStep.js +21 -24
- package/dist/cjs/global-account/react/components/SignInWithB3/steps/LoginStepCustom.js +1 -1
- package/dist/cjs/global-account/react/hooks/useAuthentication.d.ts +3 -1
- package/dist/cjs/global-account/react/hooks/useAuthentication.js +51 -7
- package/dist/cjs/global-account/react/hooks/useGetAllTWSigners.js +2 -1
- package/dist/esm/global-account/react/components/SignInWithB3/SignIn.js +1 -1
- package/dist/esm/global-account/react/components/SignInWithB3/SignInWithB3Flow.js +13 -5
- package/dist/esm/global-account/react/components/SignInWithB3/SignInWithB3Privy.js +1 -1
- package/dist/esm/global-account/react/components/SignInWithB3/steps/LoginStep.d.ts +1 -1
- package/dist/esm/global-account/react/components/SignInWithB3/steps/LoginStep.js +21 -24
- package/dist/esm/global-account/react/components/SignInWithB3/steps/LoginStepCustom.js +1 -1
- package/dist/esm/global-account/react/hooks/useAuthentication.d.ts +3 -1
- package/dist/esm/global-account/react/hooks/useAuthentication.js +51 -7
- package/dist/esm/global-account/react/hooks/useGetAllTWSigners.js +2 -1
- package/dist/types/global-account/react/components/SignInWithB3/steps/LoginStep.d.ts +1 -1
- package/dist/types/global-account/react/hooks/useAuthentication.d.ts +3 -1
- package/package.json +1 -1
- package/src/global-account/react/components/SignInWithB3/SignIn.tsx +1 -1
- package/src/global-account/react/components/SignInWithB3/SignInWithB3Flow.tsx +13 -5
- package/src/global-account/react/components/SignInWithB3/SignInWithB3Privy.tsx +1 -1
- package/src/global-account/react/components/SignInWithB3/steps/LoginStep.tsx +35 -25
- package/src/global-account/react/components/SignInWithB3/steps/LoginStepCustom.tsx +1 -1
- package/src/global-account/react/hooks/useAuthentication.ts +51 -8
- 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
|
-
|
|
21
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
18
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
|
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
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|