Skip to content

Instantly share code, notes, and snippets.

@rafinskipg
Last active October 22, 2025 16:39
Show Gist options
  • Select an option

  • Save rafinskipg/08bd74d4ee4174b4dccf2aeb3305b2d8 to your computer and use it in GitHub Desktop.

Select an option

Save rafinskipg/08bd74d4ee4174b4dccf2aeb3305b2d8 to your computer and use it in GitHub Desktop.
Connect with phantom

Integrating Phantom React SDK with a Custom Connect Modal

This guide shows how to integrate the Phantom React SDK into your dapp when you have your own custom connect modal.

Installation

npm install @phantom/react-sdk
# or
yarn add @phantom/react-sdk

1. Setup the Provider

Wrap 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>
  );
}

2. Build Your Custom Modal Component

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>
      )}
    </>
  );
}

3. Key Concepts

Extension Detection

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>
)}

Connect Parameters

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();

Mobile Detection

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>;
}

Connection State Management

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();

Provider Types

After connection, check which provider type was used:

const { currentProviderType } = usePhantom();

// "injected" - Connected via extension
// "embedded" - Connected via OAuth (Google/Apple/Phantom)
// null - Not connected

You 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]);

4. Auto-Connect Behavior

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);
});

5. Complete Hooks Reference

usePhantom()

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();

useConnect()

Hook to connect the wallet.

const {
  connect,              // (options?: AuthOptions) => Promise<ConnectResult>
  isConnecting,         // boolean
  error,                // Error | null
  currentProviderType,  // "injected" | "embedded" | null
  isPhantomAvailable,   // boolean
} = useConnect();

useIsExtensionInstalled()

Hook to check if Phantom extension is installed.

const {
  isLoading,  // boolean - Still checking
  isInstalled // boolean - Extension is installed
} = useIsExtensionInstalled();

useDisconnect()

Hook to disconnect the wallet.

import { useDisconnect } from "@phantom/react-sdk";

const { disconnect } = useDisconnect();

await disconnect();

useAccounts()

Hook to access wallet addresses.

import { useAccounts } from "@phantom/react-sdk";

const { accounts, isLoading } = useAccounts();
// accounts: WalletAddress[]

6. Event Handlers

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]);

7. UI Recommendations

Show extension option only when installed

{isInstalled && (
  <button onClick={connectWithInjected}>
    Continue with extension
  </button>
)}

Show "Login with Phantom" only when extension installed

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>
)}

Always show Google/Apple options

These OAuth flows work without the extension:

<button onClick={() => connectWithEmbedded("google")}>
  Continue with Google
</button>

<button onClick={() => connectWithEmbedded("apple")}>
  Continue with Apple
</button>

Mobile: Show deeplink when extension not available

{isMobile && !isInstalled && (
  <button onClick={connectWithDeeplink}>
    Open in Phantom App
  </button>
)}

8. TypeScript Support

All hooks and functions are fully typed:

import type {
  ConnectEventData,
  ConnectStartEventData,
  ConnectErrorEventData,
  DisconnectEventData,
  WalletAddress,
  AuthOptions,
  ConnectResult,
} from "@phantom/react-sdk";

Example: Minimal Implementation

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>
      )}
    </>
  );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment