-
Notifications
You must be signed in to change notification settings - Fork 542
[SDK] Add required email verification for in-app wallet #7130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,10 @@ | ||
"use client"; | ||
import { Suspense, lazy, useCallback } from "react"; | ||
import { Suspense, lazy, useCallback, useEffect, useState } from "react"; | ||
import type { Chain } from "../../../../../chains/types.js"; | ||
import type { ThirdwebClient } from "../../../../../client/client.js"; | ||
import { isEcosystemWallet } from "../../../../../wallets/ecosystem/is-ecosystem-wallet.js"; | ||
import type { Profile } from "../../../../../wallets/in-app/core/authentication/types.js"; | ||
import { getProfiles } from "../../../../../wallets/in-app/web/lib/auth/index.js"; | ||
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js"; | ||
import type { SmartWalletOptions } from "../../../../../wallets/smart/types.js"; | ||
import type { WalletId } from "../../../../../wallets/wallet-types.js"; | ||
|
@@ -13,11 +16,13 @@ import { useActiveAccount } from "../../../../core/hooks/wallets/useActiveAccoun | |
import { useActiveWallet } from "../../../../core/hooks/wallets/useActiveWallet.js"; | ||
import { useSetActiveWallet } from "../../../../core/hooks/wallets/useSetActiveWallet.js"; | ||
import { useConnectionManager } from "../../../../core/providers/connection-manager.js"; | ||
import { useProfiles } from "../../../hooks/wallets/useProfiles.js"; | ||
import { useSetSelectionData } from "../../../providers/wallet-ui-states-provider.js"; | ||
import { LoadingScreen } from "../../../wallets/shared/LoadingScreen.js"; | ||
import { WalletSelector } from "../WalletSelector.js"; | ||
import { onModalUnmount, reservedScreens } from "../constants.js"; | ||
import type { ConnectLocale } from "../locale/types.js"; | ||
import { LinkProfileScreen } from "../screens/LinkProfileScreen.js"; | ||
import { SignatureScreen } from "../screens/SignatureScreen.js"; | ||
import { StartScreen } from "../screens/StartScreen.js"; | ||
import type { WelcomeScreen } from "../screens/types.js"; | ||
|
@@ -84,45 +89,147 @@ export const ConnectModalContent = (props: { | |
const showSignatureScreen = siweAuth.requiresAuth && !siweAuth.isLoggedIn; | ||
const connectionManager = useConnectionManager(); | ||
|
||
// state to hold wallet awaiting email link | ||
const [pendingWallet, setPendingWallet] = useState<Wallet | undefined>(); | ||
|
||
// get profiles to observe email linking | ||
const profilesQuery = useProfiles({ client: props.client }); | ||
|
||
const handleConnected = useCallback( | ||
(wallet: Wallet) => { | ||
if (shouldSetActive) { | ||
setActiveWallet(wallet); | ||
} else { | ||
connectionManager.addConnectedWallet(wallet); | ||
} | ||
async (wallet: Wallet) => { | ||
// we will only set active wallet and call onConnect once requirements are met | ||
const finalizeConnection = (w: Wallet) => { | ||
if (shouldSetActive) { | ||
setActiveWallet(w); | ||
} else { | ||
connectionManager.addConnectedWallet(w); | ||
} | ||
|
||
if (props.onConnect) { | ||
props.onConnect(w); | ||
} | ||
|
||
onModalUnmount(() => { | ||
setSelectionData({}); | ||
setScreen(initialScreen); | ||
setModalVisibility(true); | ||
}); | ||
}; | ||
|
||
// ---------------------------------------------------------------- | ||
// Enforce required profile linking (currently only "email") | ||
// ---------------------------------------------------------------- | ||
type WalletConfig = { | ||
auth?: { | ||
required?: string[]; | ||
}; | ||
partnerId?: string; | ||
}; | ||
|
||
const walletWithConfig = wallet as unknown as { | ||
getConfig?: () => WalletConfig | undefined; | ||
}; | ||
|
||
const walletConfig = walletWithConfig.getConfig | ||
? walletWithConfig.getConfig() | ||
: undefined; | ||
const required = walletConfig?.auth?.required as string[] | undefined; | ||
const requiresEmail = required?.includes("email"); | ||
|
||
if (props.onConnect) { | ||
props.onConnect(wallet); | ||
console.log("wallet", walletConfig); | ||
|
||
console.log("requiresEmail", requiresEmail); | ||
|
||
if (requiresEmail) { | ||
try { | ||
const ecosystem = isEcosystemWallet(wallet) | ||
? { id: wallet.id, partnerId: walletConfig?.partnerId } | ||
: undefined; | ||
|
||
const profiles = await getProfiles({ | ||
client: props.client, | ||
ecosystem, | ||
}); | ||
|
||
console.log("profiles", profiles); | ||
|
||
const hasEmail = (profiles as Profile[]).some( | ||
(p) => !!p.details.email, | ||
); | ||
|
||
console.log("hasEmail", hasEmail); | ||
|
||
if (!hasEmail) { | ||
setPendingWallet(wallet); | ||
setScreen(reservedScreens.linkProfile); | ||
return; // defer activation until linked | ||
} | ||
} catch (err) { | ||
console.error("Failed to fetch profiles for required linking", err); | ||
// if fetching profiles fails, just continue the normal flow | ||
} | ||
} | ||
|
||
onModalUnmount(() => { | ||
setSelectionData({}); | ||
setModalVisibility(true); | ||
}); | ||
// ---------------------------------------------------------------- | ||
// Existing behavior (sign in step / close modal) | ||
// ---------------------------------------------------------------- | ||
|
||
// show sign in screen if required | ||
if (showSignatureScreen) { | ||
setScreen(reservedScreens.signIn); | ||
} else { | ||
setScreen(initialScreen); | ||
finalizeConnection(wallet); | ||
onClose?.(); | ||
} | ||
}, | ||
[ | ||
setModalVisibility, | ||
onClose, | ||
props.onConnect, | ||
shouldSetActive, | ||
setActiveWallet, | ||
showSignatureScreen, | ||
setScreen, | ||
connectionManager, | ||
props.onConnect, | ||
setSelectionData, | ||
shouldSetActive, | ||
setModalVisibility, | ||
props.client, | ||
setScreen, | ||
showSignatureScreen, | ||
initialScreen, | ||
connectionManager, | ||
onClose, | ||
], | ||
); | ||
|
||
// Effect to watch for email linking completion | ||
useEffect(() => { | ||
if (!pendingWallet) { | ||
return; | ||
} | ||
const profiles = profilesQuery.data; | ||
if (!profiles) { | ||
return; | ||
} | ||
const hasEmail = profiles.some((p) => !!p.details.email); | ||
if (hasEmail) { | ||
// finalize connection now | ||
if (shouldSetActive) { | ||
setActiveWallet(pendingWallet); | ||
} else { | ||
connectionManager.addConnectedWallet(pendingWallet); | ||
} | ||
props.onConnect?.(pendingWallet); | ||
setPendingWallet(undefined); | ||
setScreen(initialScreen); | ||
onClose?.(); | ||
} | ||
}, [ | ||
profilesQuery.data, | ||
pendingWallet, | ||
shouldSetActive, | ||
setActiveWallet, | ||
connectionManager, | ||
props.onConnect, | ||
setScreen, | ||
initialScreen, | ||
onClose, | ||
]); | ||
Comment on lines
+199
to
+231
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consolidate duplicated “finalize connection” logic
Extract const finalizeConnection = useCallback((wallet: Wallet) => {
if (shouldSetActive) {
setActiveWallet(wallet);
} else {
connectionManager.addConnectedWallet(wallet);
}
props.onConnect?.(wallet);
}, [shouldSetActive, setActiveWallet, connectionManager, props.onConnect]); Then call 🤖 Prompt for AI Agents
|
||
|
||
const handleBack = useCallback(() => { | ||
setSelectionData({}); | ||
setScreen(initialScreen); | ||
|
@@ -145,7 +252,9 @@ export const ConnectModalContent = (props: { | |
onShowAll={() => { | ||
setScreen(reservedScreens.showAll); | ||
}} | ||
done={handleConnected} | ||
done={async (w) => { | ||
await handleConnected(w); | ||
}} | ||
goBack={props.wallets.length > 1 ? handleBack : undefined} | ||
setModalVisibility={setModalVisibility} | ||
client={props.client} | ||
|
@@ -195,8 +304,8 @@ export const ConnectModalContent = (props: { | |
<SmartConnectUI | ||
key={wallet.id} | ||
accountAbstraction={props.accountAbstraction} | ||
done={(smartWallet) => { | ||
handleConnected(smartWallet); | ||
done={async (smartWallet) => { | ||
await handleConnected(smartWallet); | ||
}} | ||
personalWallet={wallet} | ||
onBack={goBack} | ||
|
@@ -217,8 +326,8 @@ export const ConnectModalContent = (props: { | |
key={wallet.id} | ||
wallet={wallet} | ||
onBack={goBack} | ||
done={() => { | ||
handleConnected(wallet); | ||
done={async () => { | ||
await handleConnected(wallet); | ||
}} | ||
setModalVisibility={props.setModalVisibility} | ||
chain={props.chain} | ||
|
@@ -245,6 +354,16 @@ export const ConnectModalContent = (props: { | |
/> | ||
); | ||
|
||
const linkProfileScreen = ( | ||
<LinkProfileScreen | ||
onBack={handleBack} | ||
locale={props.connectLocale} | ||
client={props.client} | ||
walletConnect={props.walletConnect} | ||
wallet={pendingWallet} | ||
/> | ||
); | ||
|
||
return ( | ||
<ScreenSetupContext.Provider value={props.screenSetup}> | ||
{props.size === "wide" ? ( | ||
|
@@ -256,6 +375,7 @@ export const ConnectModalContent = (props: { | |
{screen === reservedScreens.main && getStarted} | ||
{screen === reservedScreens.getStarted && getStarted} | ||
{screen === reservedScreens.showAll && showAll} | ||
{screen === reservedScreens.linkProfile && linkProfileScreen} | ||
{typeof screen !== "string" && getWalletUI(screen)} | ||
</> | ||
} | ||
|
@@ -266,6 +386,7 @@ export const ConnectModalContent = (props: { | |
{screen === reservedScreens.main && walletList} | ||
{screen === reservedScreens.getStarted && getStarted} | ||
{screen === reservedScreens.showAll && showAll} | ||
{screen === reservedScreens.linkProfile && linkProfileScreen} | ||
{typeof screen !== "string" && getWalletUI(screen)} | ||
</ConnectModalCompactLayout> | ||
)} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Harden the
required
-field parsingwalletConfig?.auth?.required
is cast tostring[] | undefined
without validation and then used directly. If a malformed wallet config (e.g.required: "email"
) is returned, the subsequent.includes()
will throw or yield unexpected results.A simple guard prevents runtime errors and removes the need for a type-cast.
📝 Committable suggestion
🤖 Prompt for AI Agents