Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.3.1",
"@thirdweb-dev/wagmi-adapter": "0.2.165",
"@turnkey/react-wallet-kit": "^1.6.3",
"@web3icons/react": "3.16.0",
"big.js": "^7.0.1",
"class-variance-authority": "0.7.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export function SignInWithB3Flow({
},
initialEmail: email,
skipToOtp: !!(hasTurnkeyId && hasTurnkeyEmail),
enableWalletAuth: true, // Enable wallet authentication option
closable: false, // Turnkey modal cannot be closed until auth is complete
});
return;
Expand Down Expand Up @@ -404,6 +405,7 @@ export function SignInWithB3Flow({
}}
initialEmail=""
skipToOtp={false}
enableWalletAuth={true}
/>
</LoginStepContainer>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
import React, { useEffect, useRef, useState } from "react";
import { useTurnkeyAuth } from "../hooks/useTurnkeyAuth";

type ModalStep = "email" | "otp" | "success";
type ModalStep = "method" | "email" | "wallet" | "otp" | "success";

interface WalletProvider {
name: string;
icon?: string;
address: string;
chain: string;
}

interface TurnkeyAuthModalProps {
onClose: () => void;
onSuccess: (_user: any) => void;
initialEmail?: string;
skipToOtp?: boolean;
enableWalletAuth?: boolean; // New prop to enable wallet authentication
}

export function TurnkeyAuthModal({ onClose, onSuccess, initialEmail = "", skipToOtp = false }: TurnkeyAuthModalProps) {
const [step, setStep] = useState<ModalStep>(skipToOtp ? "otp" : "email");
export function TurnkeyAuthModal({
onClose,
onSuccess,
initialEmail = "",
skipToOtp = false,
enableWalletAuth = false,
}: TurnkeyAuthModalProps) {
const [step, setStep] = useState<ModalStep>(skipToOtp ? "otp" : enableWalletAuth ? "method" : "email");
const [email, setEmail] = useState(initialEmail);
const [otpCode, setOtpCode] = useState("");
const [otpId, setOtpId] = useState("");
const [walletProviders, setWalletProviders] = useState<WalletProvider[]>([]);
const [selectedWallet, setSelectedWallet] = useState<WalletProvider | null>(null);
const [walletError, setWalletError] = useState<string | null>(null);
const autoSubmitTriggeredRef = useRef(false);

const { initiateLogin, verifyOtp, isLoading, error, clearError } = useTurnkeyAuth();
Expand Down Expand Up @@ -94,11 +111,158 @@ export function TurnkeyAuthModal({ onClose, onSuccess, initialEmail = "", skipTo
}
};

// Fetch available wallet providers when wallet step is shown
const fetchWallets = async () => {
try {
setWalletError(null);
// Check if user has MetaMask or other injected wallets
if (typeof window !== "undefined" && (window as any).ethereum) {
const ethereum = (window as any).ethereum;
const accounts = await ethereum.request({ method: "eth_requestAccounts" });
Comment on lines +119 to +121
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Directly accessing window.ethereum can be less robust than using thirdweb's wallet connectors, which are already integrated into the SDK. Leveraging thirdweb's utilities for wallet detection and connection would provide a more consistent and resilient experience, handling various wallet injection scenarios and potential edge cases more gracefully.


if (accounts && accounts.length > 0) {
setWalletProviders([
{
name: ethereum.isMetaMask ? "MetaMask" : "Ethereum Wallet",
address: accounts[0],
chain: "ethereum",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The chain property is hardcoded to "ethereum" here. If the SDK is intended to support multiple EVM chains, this should be made dynamic to reflect the currently active chain or allow for selection of other supported chains. Hardcoding it limits the flexibility and potential multi-chain capabilities of the wallet authentication feature.

},
]);
} else {
setWalletError("No wallet accounts found. Please connect your wallet.");
}
} else {
setWalletError("No Ethereum wallet detected. Please install MetaMask or another Web3 wallet.");
}
} catch (err: any) {
console.error("Failed to fetch wallets:", err);
setWalletError(err.message || "Failed to connect to wallet");
}
};

// Handle wallet authentication
const handleWalletAuth = async (provider: WalletProvider) => {
try {
setWalletError(null);
setSelectedWallet(provider);

// For now, we'll need to implement the actual Turnkey wallet auth
// This would involve signing a message with the wallet and sending it to the backend
// TODO: Implement full wallet authentication flow with Turnkey
setWalletError("Wallet authentication coming soon. Please use email authentication for now.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The TODO comment and the user-facing message "Wallet authentication coming soon. Please use email authentication for now." indicate that the wallet authentication feature is not yet functional. Presenting a non-functional option to users can lead to a poor experience. Consider either:

  1. Disabling the option: If the feature is not ready, it might be better to disable the "External Wallet" button or hide it entirely until it's fully implemented.
  2. Clearer communication: If it must be shown, ensure the message is very prominent and explains why it's not available (e.g., "This feature is under development and will be available soon.").

Given this is a [WIP] PR, this might be acceptable for now, but it's a high-priority item before merging to main.

} catch (err: any) {
console.error("Failed to authenticate with wallet:", err);
setWalletError(err.message || "Failed to authenticate with wallet");
}
};

const isTurnkeyPrimary = process.env.NEXT_PUBLIC_TURNKEY_PRIMARY === "true";
const walletBrand = isTurnkeyPrimary ? "Smart Wallet" : "AnySpend Wallet";

return (
<div className="font-neue-montreal p-8">
{/* Method Selection Step */}
{step === "method" && (
<>
<h2 className="mb-6 text-center text-2xl font-bold text-gray-900 dark:text-white">
Setup your {walletBrand}
</h2>
<div className="mb-6 space-y-3 text-center text-sm text-gray-600 dark:text-gray-400">
<p>Choose your authentication method</p>
</div>

<div className="space-y-3">
<button
onClick={() => setStep("email")}
className="w-full rounded-lg border-2 border-gray-300 bg-white px-6 py-4 font-semibold text-gray-900 transition-all duration-200 hover:border-blue-500 hover:bg-blue-50 dark:border-gray-700 dark:bg-gray-800 dark:text-white dark:hover:border-blue-500 dark:hover:bg-gray-700"
>
<div className="flex items-center justify-center gap-3">
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
Email + OTP
</div>
</button>

<button
onClick={() => {
setStep("wallet");
fetchWallets();
}}
className="w-full rounded-lg border-2 border-gray-300 bg-white px-6 py-4 font-semibold text-gray-900 transition-all duration-200 hover:border-blue-500 hover:bg-blue-50 dark:border-gray-700 dark:bg-gray-800 dark:text-white dark:hover:border-blue-500 dark:hover:bg-gray-700"
>
<div className="flex items-center justify-center gap-3">
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
External Wallet
</div>
</button>
</div>
</>
)}

{/* Wallet Selection Step */}
{step === "wallet" && (
<>
<h2 className="mb-6 text-center text-2xl font-bold text-gray-900 dark:text-white">Connect Wallet</h2>
<div className="mb-6 space-y-3 text-center text-sm text-gray-600 dark:text-gray-400">
<p>Select a wallet to authenticate</p>
</div>

{walletError && (
<div className="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400">
{walletError}
</div>
)}

<div className="space-y-3">
{walletProviders.length > 0 ? (
walletProviders.map((provider, index) => (
<button
key={index}
onClick={() => handleWalletAuth(provider)}
className="w-full rounded-lg border-2 border-gray-300 bg-white px-6 py-4 font-semibold text-gray-900 transition-all duration-200 hover:border-blue-500 hover:bg-blue-50 dark:border-gray-700 dark:bg-gray-800 dark:text-white dark:hover:border-blue-500 dark:hover:bg-gray-700"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{provider.icon && <img src={provider.icon} alt={provider.name} className="h-8 w-8" />}
<div className="text-left">
<div className="font-semibold">{provider.name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
{provider.address.slice(0, 6)}...{provider.address.slice(-4)}
</div>
</div>
</div>
</div>
</button>
))
) : (
<div className="flex min-h-[200px] items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-600 border-t-transparent"></div>
</div>
)}
</div>

<button
onClick={() => setStep("method")}
className="mt-4 w-full text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
>
← Back to methods
</button>
</>
)}

{/* Email Step */}
{step === "email" && (
<>
Expand Down Expand Up @@ -152,6 +316,15 @@ export function TurnkeyAuthModal({ onClose, onSuccess, initialEmail = "", skipTo
)}
</button>
</form>

{enableWalletAuth && (
<button
onClick={() => setStep("method")}
className="mt-4 w-full text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
>
← Back to methods
</button>
)}
</>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export interface TurnkeyAuthModalProps extends BaseModalProps {
initialEmail?: string;
/** Whether to skip directly to OTP step */
skipToOtp?: boolean;
/** Whether to enable wallet authentication option */
enableWalletAuth?: boolean;
/** Whether the modal can be closed - defaults to false for Turnkey */
closable?: boolean;
}
Expand Down
Loading
Loading