This guide shows how to integrate the Phantom React SDK into your dapp when you have your own custom connect modal.
npm install @phantom/react-sdk
# or
yarn add @phantom/react-sdkWrap your app with PhantomProvider at the root level:
import { PhantomProvider } from "@phantom/react-sdk";
function App() {
return (
<PhantomProvider
config={{
appId: "your-app-id",
organizationId: "your-org-id",
apiBaseUrl: "https://api.phantom.app",
embeddedWalletType: "user-wallet",
addressTypes: ["solana", "ethereum"],
autoConnect: true, // Auto-reconnect on page load
}}
>
<YourApp />
</PhantomProvider>
);
}import { useState } from "react";
import {
usePhantom,
useConnect,
useIsExtensionInstalled
} from "@phantom/react-sdk";
import { isMobileDevice, getDeeplinkToPhantom } from "@phantom/browser-sdk";
function CustomConnectModal() {
const { sdk, isConnected, addresses, currentProviderType } = usePhantom();
const { connect, isConnecting, error } = useConnect();
const { isLoading, isInstalled } = useIsExtensionInstalled();
const isMobile = isMobileDevice();
const [showModal, setShowModal] = useState(false);
// Connect with embedded provider (OAuth flow)
const connectWithEmbedded = async (provider?: "google" | "apple" | "phantom") => {
try {
const authOptions = provider ? { provider } : undefined;
await connect(authOptions);
setShowModal(false);
} catch (err) {
console.error("Connection failed:", err);
}
};
// Connect with injected provider (extension)
const connectWithInjected = async () => {
try {
await connect({ provider: "injected" });
setShowModal(false);
} catch (err) {
console.error("Connection failed:", err);
}
};
// Connect with deeplink (mobile redirect)
const connectWithDeeplink = () => {
const deeplinkUrl = getDeeplinkToPhantom();
window.location.href = deeplinkUrl;
};
return (
<>
{/* Your connect button */}
<button onClick={() => setShowModal(true)}>
{isConnected
? `Connected: ${addresses[0]?.address.slice(0, 8)}...`
: "Connect Wallet"}
</button>
{/* Your custom modal */}
{showModal && (
<div className="modal-overlay" onClick={() => setShowModal(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2>Connect to Phantom</h2>
{error && <div className="error">{error.message}</div>}
{/* MOBILE: Show deeplink if extension not installed */}
{isMobile && !isInstalled && (
<button
onClick={connectWithDeeplink}
disabled={isConnecting}
>
Open in Phantom App
</button>
)}
{/* DESKTOP: Show connection options */}
{!isMobile && (
<>
{/* Option 1: Login with Phantom (embedded) - Only show if extension exists */}
{isInstalled && (
<button
onClick={() => connectWithEmbedded("phantom")}
disabled={isConnecting}
className="primary-button"
>
{isConnecting ? "Connecting..." : "Login with Phantom"}
</button>
)}
{/* Option 2: Continue with Google (embedded) */}
<button
onClick={() => connectWithEmbedded("google")}
disabled={isConnecting}
>
{isConnecting ? "Connecting..." : "Continue with Google"}
</button>
{/* Option 3: Continue with Apple (embedded) */}
<button
onClick={() => connectWithEmbedded("apple")}
disabled={isConnecting}
>
{isConnecting ? "Connecting..." : "Continue with Apple"}
</button>
{/* Option 4: Use extension directly - Only show if extension exists */}
{isInstalled && (
<>
<div className="divider">or</div>
<button
onClick={connectWithInjected}
disabled={isConnecting}
className="secondary-button"
>
{isConnecting ? "Connecting..." : "Continue with extension"}
</button>
</>
)}
</>
)}
</div>
</div>
)}
</>
);
}Always check if the extension is installed before rendering extension-related buttons:
const { isLoading, isInstalled } = useIsExtensionInstalled();
// Wait for loading to complete before showing UI
if (isLoading) {
return <div>Loading...</div>;
}
// Only render extension buttons when installed
{isInstalled && (
<button onClick={connectWithInjected}>
Continue with extension
</button>
)}The connect() function accepts an AuthOptions object with a provider field:
| Provider Value | Description | Use Case |
|---|---|---|
"injected" |
Use Phantom browser extension | User has extension installed, direct wallet connection |
"google" |
OAuth flow with Google | Embedded wallet using Google account |
"apple" |
OAuth flow with Apple | Embedded wallet using Apple ID |
"phantom" |
OAuth flow with Phantom account | Embedded wallet using existing Phantom account |
undefined |
Default Phantom Connect UI | Shows Phantom's default connection UI |
Examples:
// Connect with extension
await connect({ provider: "injected" });
// Connect with Google OAuth
await connect({ provider: "google" });
// Connect with Apple OAuth
await connect({ provider: "apple" });
// Connect with Phantom account
await connect({ provider: "phantom" });
// Default Phantom Connect flow
await connect();On mobile devices, use deeplink to redirect to Phantom app:
import { isMobileDevice, getDeeplinkToPhantom } from "@phantom/browser-sdk";
const isMobile = isMobileDevice();
if (isMobile && !isInstalled) {
// Show deeplink button instead of embedded options
const handleDeeplink = () => {
const deeplinkUrl = getDeeplinkToPhantom();
window.location.href = deeplinkUrl;
};
return <button onClick={handleDeeplink}>Open in Phantom App</button>;
}The SDK automatically manages connection state through events:
const {
isConnected, // Boolean: Is wallet connected?
isConnecting, // Boolean: Is connection in progress?
connectError, // Error | null: Connection error if any
addresses, // WalletAddress[]: Connected wallet addresses
currentProviderType // "injected" | "embedded" | null: Current provider
} = usePhantom();After connection, check which provider type was used:
const { currentProviderType } = usePhantom();
// "injected" - Connected via extension
// "embedded" - Connected via OAuth (Google/Apple/Phantom)
// null - Not connectedYou can also listen to connect events to get provider type:
import { useEffect } from "react";
useEffect(() => {
if (!sdk) return;
const handleConnect = (data: ConnectEventData) => {
console.log("Connected with provider:", data.providerType);
console.log("Wallet ID:", data.walletId);
console.log("Addresses:", data.addresses);
};
sdk.on("connect", handleConnect);
return () => {
sdk.off("connect", handleConnect);
};
}, [sdk]);When autoConnect: true is set in config, the SDK will automatically reconnect on page load if a valid session exists.
The connect event will fire with source: "auto-connect" or source: "existing-session":
sdk.on("connect", (data) => {
console.log("Connect source:", data.source);
// "auto-connect" - Reconnected from existing session
// "manual-connect" - User clicked connect button
// "existing-session" - Already had valid session
console.log("Provider type:", data.providerType);
});Main hook to access SDK instance and connection state.
const {
sdk, // BrowserSDK | null
isConnected, // boolean
isConnecting, // boolean
connectError, // Error | null
addresses, // WalletAddress[]
currentProviderType, // "injected" | "embedded" | null
isPhantomAvailable, // boolean (for injected provider)
isClient, // boolean (SSR safety)
} = usePhantom();Hook to connect the wallet.
const {
connect, // (options?: AuthOptions) => Promise<ConnectResult>
isConnecting, // boolean
error, // Error | null
currentProviderType, // "injected" | "embedded" | null
isPhantomAvailable, // boolean
} = useConnect();Hook to check if Phantom extension is installed.
const {
isLoading, // boolean - Still checking
isInstalled // boolean - Extension is installed
} = useIsExtensionInstalled();Hook to disconnect the wallet.
import { useDisconnect } from "@phantom/react-sdk";
const { disconnect } = useDisconnect();
await disconnect();Hook to access wallet addresses.
import { useAccounts } from "@phantom/react-sdk";
const { accounts, isLoading } = useAccounts();
// accounts: WalletAddress[]The SDK emits typed events that you can listen to:
import type { ConnectEventData } from "@phantom/react-sdk";
useEffect(() => {
if (!sdk) return;
// Connect started
sdk.on("connect_start", (data) => {
console.log("Connection starting:", data.source);
});
// Connected successfully
sdk.on("connect", (data: ConnectEventData) => {
console.log("Connected:", data.providerType);
console.log("Addresses:", data.addresses);
console.log("Wallet ID:", data.walletId);
});
// Connection error
sdk.on("connect_error", (data) => {
console.error("Connection error:", data.error);
});
// Disconnected
sdk.on("disconnect", (data) => {
console.log("Disconnected:", data.source);
});
// Cleanup
return () => {
sdk.off("connect_start", handleConnectStart);
sdk.off("connect", handleConnect);
sdk.off("connect_error", handleConnectError);
sdk.off("disconnect", handleDisconnect);
};
}, [sdk]);{isInstalled && (
<button onClick={connectWithInjected}>
Continue with extension
</button>
)}The "Login with Phantom" button uses the embedded provider but requires the extension to be installed for the OAuth flow.
{isInstalled && (
<button onClick={() => connectWithEmbedded("phantom")}>
Login with Phantom
</button>
)}These OAuth flows work without the extension:
<button onClick={() => connectWithEmbedded("google")}>
Continue with Google
</button>
<button onClick={() => connectWithEmbedded("apple")}>
Continue with Apple
</button>{isMobile && !isInstalled && (
<button onClick={connectWithDeeplink}>
Open in Phantom App
</button>
)}All hooks and functions are fully typed:
import type {
ConnectEventData,
ConnectStartEventData,
ConnectErrorEventData,
DisconnectEventData,
WalletAddress,
AuthOptions,
ConnectResult,
} from "@phantom/react-sdk";import { useState } from "react";
import { useConnect, useIsExtensionInstalled } from "@phantom/react-sdk";
import { isMobileDevice, getDeeplinkToPhantom } from "@phantom/browser-sdk";
function ConnectButton() {
const { connect, isConnecting } = useConnect();
const { isInstalled } = useIsExtensionInstalled();
const [showModal, setShowModal] = useState(false);
const isMobile = isMobileDevice();
return (
<>
<button onClick={() => setShowModal(true)}>Connect</button>
{showModal && (
<div className="modal">
{isMobile && !isInstalled ? (
<button onClick={() => window.location.href = getDeeplinkToPhantom()}>
Open Phantom App
</button>
) : (
<>
{isInstalled && (
<button onClick={() => connect({ provider: "injected" })}>
Use Extension
</button>
)}
<button onClick={() => connect({ provider: "google" })}>
Continue with Google
</button>
</>
)}
</div>
)}
</>
);
}