Skip to content

Commit e5aa4b9

Browse files
[SDK] Add required email verification for in-app wallet
1 parent 23029db commit e5aa4b9

File tree

9 files changed

+278
-32
lines changed

9 files changed

+278
-32
lines changed

apps/playground-web/src/components/in-app-wallet/connect-button.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"use client";
22

33
import { inAppWallet } from "thirdweb/wallets/in-app";
4-
import { StyledConnectEmbed } from "../styled-connect-embed";
4+
import { StyledConnectButton } from "../styled-connect-button";
55

66
export function InAppConnectEmbed() {
77
return (
8-
<StyledConnectEmbed
8+
<StyledConnectButton
99
wallets={[
1010
inAppWallet({
1111
auth: {
@@ -24,6 +24,7 @@ export function InAppConnectEmbed() {
2424
"passkey",
2525
"guest",
2626
],
27+
required: ["email"],
2728
},
2829
}),
2930
]}

apps/playground-web/src/components/in-app-wallet/sponsored-tx.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export function SponsoredInAppTxPreview() {
4343
"passkey",
4444
"guest",
4545
],
46+
required: ["email"],
4647
},
4748
// TODO (7702): update to 7702 once pectra is out
4849
executionMode: {

apps/playground-web/src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const WALLETS = [
2727
"farcaster",
2828
"line",
2929
],
30+
required: ["email"],
3031
mode: "redirect",
3132
passkeyDomain: getDomain(),
3233
},

packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/ConnectEmbed.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useActiveAccount } from "../../../../core/hooks/wallets/useActiveAccoun
1919
import { useActiveWallet } from "../../../../core/hooks/wallets/useActiveWallet.js";
2020
import { useIsAutoConnecting } from "../../../../core/hooks/wallets/useIsAutoConnecting.js";
2121
import { useConnectionManager } from "../../../../core/providers/connection-manager.js";
22+
import { useProfiles } from "../../../hooks/wallets/useProfiles.js";
2223
import { WalletUIStatesProvider } from "../../../providers/wallet-ui-states-provider.js";
2324
import { canFitWideModal } from "../../../utils/canFitWideModal.js";
2425
import { usePreloadWalletProviders } from "../../../utils/usePreloadWalletProviders.js";
@@ -184,8 +185,38 @@ export function ConnectEmbed(props: ConnectEmbedProps) {
184185
const activeWallet = useActiveWallet();
185186
const activeAccount = useActiveAccount();
186187
const siweAuth = useSiweAuth(activeWallet, activeAccount, props.auth);
188+
189+
// profiles for linking requirement
190+
const { data: profiles } = useProfiles({ client: props.client });
191+
192+
// Determine if wallet requires an email and user doesn't have one linked
193+
const needsEmailLink = (() => {
194+
if (!activeWallet || !profiles) {
195+
return false;
196+
}
197+
198+
const walletConfig = (
199+
activeWallet as unknown as {
200+
getConfig?: () => { auth?: { required?: string[] } } | undefined;
201+
}
202+
).getConfig?.();
203+
204+
const required = walletConfig?.auth?.required || [];
205+
const requiresEmail = required.includes("email");
206+
207+
if (!requiresEmail) {
208+
return false;
209+
}
210+
211+
const hasEmail = profiles.some((p) => !!p.details.email);
212+
213+
return !hasEmail;
214+
})();
215+
187216
const show =
188-
!activeAccount || (siweAuth.requiresAuth && !siweAuth.isLoggedIn);
217+
!activeAccount ||
218+
(siweAuth.requiresAuth && !siweAuth.isLoggedIn) ||
219+
needsEmailLink;
189220
const connectionManager = useConnectionManager();
190221

191222
// Add props.chain and props.chains to defined chains store

packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/ConnectModalContent.tsx

Lines changed: 148 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"use client";
2-
import { Suspense, lazy, useCallback } from "react";
2+
import { Suspense, lazy, useCallback, useEffect, useState } from "react";
33
import type { Chain } from "../../../../../chains/types.js";
44
import type { ThirdwebClient } from "../../../../../client/client.js";
5+
import { isEcosystemWallet } from "../../../../../wallets/ecosystem/is-ecosystem-wallet.js";
6+
import type { Profile } from "../../../../../wallets/in-app/core/authentication/types.js";
7+
import { getProfiles } from "../../../../../wallets/in-app/web/lib/auth/index.js";
58
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
69
import type { SmartWalletOptions } from "../../../../../wallets/smart/types.js";
710
import type { WalletId } from "../../../../../wallets/wallet-types.js";
@@ -13,11 +16,13 @@ import { useActiveAccount } from "../../../../core/hooks/wallets/useActiveAccoun
1316
import { useActiveWallet } from "../../../../core/hooks/wallets/useActiveWallet.js";
1417
import { useSetActiveWallet } from "../../../../core/hooks/wallets/useSetActiveWallet.js";
1518
import { useConnectionManager } from "../../../../core/providers/connection-manager.js";
19+
import { useProfiles } from "../../../hooks/wallets/useProfiles.js";
1620
import { useSetSelectionData } from "../../../providers/wallet-ui-states-provider.js";
1721
import { LoadingScreen } from "../../../wallets/shared/LoadingScreen.js";
1822
import { WalletSelector } from "../WalletSelector.js";
1923
import { onModalUnmount, reservedScreens } from "../constants.js";
2024
import type { ConnectLocale } from "../locale/types.js";
25+
import { LinkProfileScreen } from "../screens/LinkProfileScreen.js";
2126
import { SignatureScreen } from "../screens/SignatureScreen.js";
2227
import { StartScreen } from "../screens/StartScreen.js";
2328
import type { WelcomeScreen } from "../screens/types.js";
@@ -84,45 +89,147 @@ export const ConnectModalContent = (props: {
8489
const showSignatureScreen = siweAuth.requiresAuth && !siweAuth.isLoggedIn;
8590
const connectionManager = useConnectionManager();
8691

92+
// state to hold wallet awaiting email link
93+
const [pendingWallet, setPendingWallet] = useState<Wallet | undefined>();
94+
95+
// get profiles to observe email linking
96+
const profilesQuery = useProfiles({ client: props.client });
97+
8798
const handleConnected = useCallback(
88-
(wallet: Wallet) => {
89-
if (shouldSetActive) {
90-
setActiveWallet(wallet);
91-
} else {
92-
connectionManager.addConnectedWallet(wallet);
93-
}
99+
async (wallet: Wallet) => {
100+
// we will only set active wallet and call onConnect once requirements are met
101+
const finalizeConnection = (w: Wallet) => {
102+
if (shouldSetActive) {
103+
setActiveWallet(w);
104+
} else {
105+
connectionManager.addConnectedWallet(w);
106+
}
107+
108+
if (props.onConnect) {
109+
props.onConnect(w);
110+
}
111+
112+
onModalUnmount(() => {
113+
setSelectionData({});
114+
setScreen(initialScreen);
115+
setModalVisibility(true);
116+
});
117+
};
118+
119+
// ----------------------------------------------------------------
120+
// Enforce required profile linking (currently only "email")
121+
// ----------------------------------------------------------------
122+
type WalletConfig = {
123+
auth?: {
124+
required?: string[];
125+
};
126+
partnerId?: string;
127+
};
128+
129+
const walletWithConfig = wallet as unknown as {
130+
getConfig?: () => WalletConfig | undefined;
131+
};
132+
133+
const walletConfig = walletWithConfig.getConfig
134+
? walletWithConfig.getConfig()
135+
: undefined;
136+
const required = walletConfig?.auth?.required as string[] | undefined;
137+
const requiresEmail = required?.includes("email");
94138

95-
if (props.onConnect) {
96-
props.onConnect(wallet);
139+
console.log("wallet", walletConfig);
140+
141+
console.log("requiresEmail", requiresEmail);
142+
143+
if (requiresEmail) {
144+
try {
145+
const ecosystem = isEcosystemWallet(wallet)
146+
? { id: wallet.id, partnerId: walletConfig?.partnerId }
147+
: undefined;
148+
149+
const profiles = await getProfiles({
150+
client: props.client,
151+
ecosystem,
152+
});
153+
154+
console.log("profiles", profiles);
155+
156+
const hasEmail = (profiles as Profile[]).some(
157+
(p) => !!p.details.email,
158+
);
159+
160+
console.log("hasEmail", hasEmail);
161+
162+
if (!hasEmail) {
163+
setPendingWallet(wallet);
164+
setScreen(reservedScreens.linkProfile);
165+
return; // defer activation until linked
166+
}
167+
} catch (err) {
168+
console.error("Failed to fetch profiles for required linking", err);
169+
// if fetching profiles fails, just continue the normal flow
170+
}
97171
}
98172

99-
onModalUnmount(() => {
100-
setSelectionData({});
101-
setModalVisibility(true);
102-
});
173+
// ----------------------------------------------------------------
174+
// Existing behavior (sign in step / close modal)
175+
// ----------------------------------------------------------------
103176

104-
// show sign in screen if required
105177
if (showSignatureScreen) {
106178
setScreen(reservedScreens.signIn);
107179
} else {
108-
setScreen(initialScreen);
180+
finalizeConnection(wallet);
109181
onClose?.();
110182
}
111183
},
112184
[
113-
setModalVisibility,
114-
onClose,
115-
props.onConnect,
185+
shouldSetActive,
116186
setActiveWallet,
117-
showSignatureScreen,
118-
setScreen,
187+
connectionManager,
188+
props.onConnect,
119189
setSelectionData,
120-
shouldSetActive,
190+
setModalVisibility,
191+
props.client,
192+
setScreen,
193+
showSignatureScreen,
121194
initialScreen,
122-
connectionManager,
195+
onClose,
123196
],
124197
);
125198

199+
// Effect to watch for email linking completion
200+
useEffect(() => {
201+
if (!pendingWallet) {
202+
return;
203+
}
204+
const profiles = profilesQuery.data;
205+
if (!profiles) {
206+
return;
207+
}
208+
const hasEmail = profiles.some((p) => !!p.details.email);
209+
if (hasEmail) {
210+
// finalize connection now
211+
if (shouldSetActive) {
212+
setActiveWallet(pendingWallet);
213+
} else {
214+
connectionManager.addConnectedWallet(pendingWallet);
215+
}
216+
props.onConnect?.(pendingWallet);
217+
setPendingWallet(undefined);
218+
setScreen(initialScreen);
219+
onClose?.();
220+
}
221+
}, [
222+
profilesQuery.data,
223+
pendingWallet,
224+
shouldSetActive,
225+
setActiveWallet,
226+
connectionManager,
227+
props.onConnect,
228+
setScreen,
229+
initialScreen,
230+
onClose,
231+
]);
232+
126233
const handleBack = useCallback(() => {
127234
setSelectionData({});
128235
setScreen(initialScreen);
@@ -145,7 +252,9 @@ export const ConnectModalContent = (props: {
145252
onShowAll={() => {
146253
setScreen(reservedScreens.showAll);
147254
}}
148-
done={handleConnected}
255+
done={async (w) => {
256+
await handleConnected(w);
257+
}}
149258
goBack={props.wallets.length > 1 ? handleBack : undefined}
150259
setModalVisibility={setModalVisibility}
151260
client={props.client}
@@ -195,8 +304,8 @@ export const ConnectModalContent = (props: {
195304
<SmartConnectUI
196305
key={wallet.id}
197306
accountAbstraction={props.accountAbstraction}
198-
done={(smartWallet) => {
199-
handleConnected(smartWallet);
307+
done={async (smartWallet) => {
308+
await handleConnected(smartWallet);
200309
}}
201310
personalWallet={wallet}
202311
onBack={goBack}
@@ -217,8 +326,8 @@ export const ConnectModalContent = (props: {
217326
key={wallet.id}
218327
wallet={wallet}
219328
onBack={goBack}
220-
done={() => {
221-
handleConnected(wallet);
329+
done={async () => {
330+
await handleConnected(wallet);
222331
}}
223332
setModalVisibility={props.setModalVisibility}
224333
chain={props.chain}
@@ -245,6 +354,16 @@ export const ConnectModalContent = (props: {
245354
/>
246355
);
247356

357+
const linkProfileScreen = (
358+
<LinkProfileScreen
359+
onBack={handleBack}
360+
locale={props.connectLocale}
361+
client={props.client}
362+
walletConnect={props.walletConnect}
363+
wallet={pendingWallet}
364+
/>
365+
);
366+
248367
return (
249368
<ScreenSetupContext.Provider value={props.screenSetup}>
250369
{props.size === "wide" ? (
@@ -256,6 +375,7 @@ export const ConnectModalContent = (props: {
256375
{screen === reservedScreens.main && getStarted}
257376
{screen === reservedScreens.getStarted && getStarted}
258377
{screen === reservedScreens.showAll && showAll}
378+
{screen === reservedScreens.linkProfile && linkProfileScreen}
259379
{typeof screen !== "string" && getWalletUI(screen)}
260380
</>
261381
}
@@ -266,6 +386,7 @@ export const ConnectModalContent = (props: {
266386
{screen === reservedScreens.main && walletList}
267387
{screen === reservedScreens.getStarted && getStarted}
268388
{screen === reservedScreens.showAll && showAll}
389+
{screen === reservedScreens.linkProfile && linkProfileScreen}
269390
{typeof screen !== "string" && getWalletUI(screen)}
270391
</ConnectModalCompactLayout>
271392
)}

packages/thirdweb/src/react/web/ui/ConnectWallet/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export const reservedScreens = {
33
getStarted: "getStarted",
44
signIn: "signIn",
55
showAll: "showAll",
6+
linkProfile: "linkProfile",
67
};
78

89
export const modalMaxWidthCompact = "360px";

packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkProfileScreen.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ export function LinkProfileScreen(props: {
2828
locale: ConnectLocale;
2929
client: ThirdwebClient;
3030
walletConnect: { projectId?: string } | undefined;
31+
wallet?: Wallet;
3132
}) {
3233
const adminWallet = useAdminWallet();
3334
const activeWallet = useActiveWallet();
3435
const chain = useActiveWalletChain();
3536
const queryClient = useQueryClient();
3637

37-
const wallet = adminWallet || activeWallet;
38+
const wallet = props.wallet || adminWallet || activeWallet;
3839

3940
if (!wallet) {
4041
return <LoadingScreen />;

0 commit comments

Comments
 (0)