How to Connect a Web3 React App to Phantom using TypeScript

If you are interested in web3 development, one of the fundamental aspects of developing a web3 app is to establish a connection between the app and a wallet. In fact, chances are this is the initial step to “authenticate” or “login” users without the need of having a database to store their account information.

In this tutorial, you are going to learn how to connect a Web3 React app to a Phantom Wallet. Although it might sound complicated, it is not as hard as it sounds. Also, take into account you don’t have to use React to connect to a Phantom wallet. Once you learn the core concepts, you can use any other JavaScript frontend framework or library to establish a connection to a Phantom wallet.

What is Phantom?

Phantom is a crypto wallet that enables users to send and receive tokens, collect NFTs, and even allow to connect to a hardware wallet. Phantom developed a browser extension to not only have quick access to your wallet but also enable web3 apps to access JavaScript solana to detect if the Phantom provider is available.

Step-by-Step: Connect a Web3 React App to a Phantom Wallet

This is the workflow of this tutorial: We are going to start by creating our React app. Then, we will detect if the Phantom provider is available. We will create a button to establish a connection to a Phantom wallet. Once we are connected to the account, we will generate another button to disconnect from the wallet.

Pre-requisites

Use Chrome, Brave, Firefox, or Edge Browser

As previously mentioned, Phantom has a browser extension to “turn your browser to a crypto wallet”, just like they say on their site. At the moment of this writing, the Phantom browser extension is available in the following browsers:

  • Chrome
  • Brave
  • Firefox
  • Edge

Therefore, make sure to use any of those browsers when testing the application.

Note: this tutorial was tested in chrome

Create a Phantom Wallet

Before we move forward, make sure you have an existing wallet. Feel free to skip this section if you already have an existing wallet you can use to establish a connection. On the other hand, if you don’t have a wallet, make sure to create one.

Install Phantom Browser Extension

Install the phantom browser extension to the browser you want to use:

Create a New Wallet

Once the Phantom Browser Extension is installed, you will be redirected to a page to create a new wallet. Click on “Create New Wallet”.

Create a New Phantom Wallet
Save Recovery Phrase

After creating your Phantom wallet, you will be given a secret recovery phrase. Save it in a safe place. I recommend using a password manager to save this information.

Save secret recovery phrase

Once you save it, click on “OK, I saved it somewhere”.

Create a password for your Phantom Wallet

After saving the recovery phrase, it will prompt you to create a password. Go ahead and create your password. Agree to terms of service and click “Continue”.

Create a password for your Phantom Wallet

Once you do this, you can access your Phantom wallet by clicking the browser extension.

Accessing wallet using the Phantom browser extension

Creating the React App using TypeScript

Time to start building the app. First, generate a new react project using Typescript using the following command.

Note: Feel free to name your project to other than web3-connect-phantom-wallet.

npx create-react-app web3-connect-phantom-wallet --template typescript

Install dependencies

For this tutorial, we need to install @solana/web3.js. Go ahead and add it.

yarn add @solana/web3.js

Detect Phantom Provider Exists

Prior to attempting to connect to a wallet, we need to first detect the existence of the wallet provider, in other words, whoever is providing with software to hold, store, or transfer digital currency. In this case, we are looking to detect if Phantom exists.

Adding PhantomProvider Interface

First, we need to generate TypeScript interfaces to easily identify all the properties of the Phantom provider inside the App.tsx file.

import {
  PublicKey,
  Transaction,
} from "@solana/web3.js";



type DisplayEncoding = "utf8" | "hex";
type PhantomEvent = "disconnect" | "connect" | "accountChanged";
type PhantomRequestMethod =
  | "connect"
  | "disconnect"
  | "signTransaction"
  | "signAllTransactions"
  | "signMessage";

interface ConnectOpts {
  onlyIfTrusted: boolean;
}

interface PhantomProvider {
  publicKey: PublicKey | null;
  isConnected: boolean | null;
  signTransaction: (transaction: Transaction) => Promise<Transaction>;
  signAllTransactions: (transactions: Transaction[]) => Promise<Transaction[]>;
  signMessage: (
    message: Uint8Array | string,
    display?: DisplayEncoding
  ) => Promise<any>;
  connect: (opts?: Partial<ConnectOpts>) => Promise<{ publicKey: PublicKey }>;
  disconnect: () => Promise<void>;
  on: (event: PhantomEvent, handler: (args: any) => void) => void;
  request: (method: PhantomRequestMethod, params: any) => Promise<unknown>;
}
Generate function to get provider

Next, add inside the App component a getProvider variable to store a function that gets a wallet provider.

  /**
   * @description gets Phantom provider, if it exists
   */
  const getProvider = (): PhantomProvider | undefined => {
    if ("solana" in window) {
      // @ts-ignore
      const provider = window.solana as any;
      if (provider.isPhantom) return provider as PhantomProvider;
    }
  };

If you have worked in web development, you will find as a surprise the existence of a solana key property as part of the window object. This doesn’t mean the window.solana property has always existed. What happens is, the Phantom browser extension injects the solana object as a key of the window object to make easy the process of detecting the existence of a wallet provider.

Having said that, if the Phantom browser extension is enabled in the browser, our app will identify the existence of the provider. On the other hand, if you decide to disable the browser extension, the app will not find the Phantom provider.

Store the state of the provider

We are going to store the state of the provider in our app. This will help us determine what to display to the user based on whether the Phantom provider exists or not.

First, import useEffect and useState from react.

import { useEffect, useState } from "react";

Then, inside the App component, generate the variables to store and update the state of the provider.

const [provider, setProvider] = useState<PhantomProvider | undefined>(
    undefined
  );

Now, add the following logic below the getProvider variable to set the state of the provider.

// detect phantom provider exists
  useEffect(() => {
    const provider = getProvider();

    if (provider) setProvider(provider);
    else setProvider(undefined);
  }, []);

Notice this code will only execute once the component mounts because we are using an empty array [] as a second argument of the useEffect function. This is important as we don’t want to constantly update the provider every time the app re-renders.

Add logic to display content based on state of the provider

Finally, update the UI based on the state of the provider. If the provider exists, add a button to connect to the Phantom wallet. Otherwise, show a message suggesting the user to install the Phantom Browser extension.

return (
    <div className="App">
      <header className="App-header">
        <h2>Tutorial: Connect to Phantom Wallet</h2>
        {provider && (
          <button
            style={{
              fontSize: "16px",
              padding: "15px",
              fontWeight: "bold",
              borderRadius: "5px",
            }}
          >
            Connect to Phantom Wallet
          </button>
        )}

        {!provider && (
          <p>
            No provider found. Install{" "}
            <a href="https://phantom.app/">Phantom Browser extension</a>
          </p>
        )}

        <p>
          Built by{" "}
          <a
            href="https://twitter.com/arealesramirez"
            target="_blank"
            rel="noreferrer"
            className="twitter-link"
          >
            @arealesramirez
          </a>
        </p>
      </header>
    </div>

Open the App.css to add some styling to the links and buttons we added.

a {
  color: white;
}
.twitter-link {
  color: "white";
  font-size: "16px";
  margin-top: "15px";
  margin-bottom: "15px";
}
Test the logic

That was quite a bit of explanation, which hopefully will make sense once we start running our app.

yarn start

Notice how the “Connect to Phantom Wallet” button is displayed whenever the Phantom browser extension is available.

Connect to Phantom Wallet button displayed

Now, go ahead and disable the Phantom browser extension from your browser. Then, refresh the page. You should see a message prompting the user to install the browser extension.

Message displayed when Phantom browser extension is not available

Once proving the logic works, enable Phantom browser extension one more time.

Connect to Phantom Wallet

We have a “Connect to Phantom Wallet” button that doesn’t do anything as of now. Let’s create a function to trigger once the user clicks the button.

First, generate a walletKey to store the public key of the connected account.

const [walletKey, setWalletKey] = useState<PhantomProvider | undefined>(
    undefined
  );

Then, add a connectWallet variable storing the function that makes the request to connect to the wallet below getProvider.

/**
   * @description prompts user to connect wallet if it exists
   */
  const connectWallet = async () => {
    // @ts-ignore
    const { solana } = window;

    if (solana) {
      try {
        const response = await solana.connect();
        console.log('wallet account ', response.publicKey.toString());
        setWalletKey(response.publicKey.toString());
      } catch (err) {
       // { code: 4001, message: 'User rejected the request.' }
      }
    }
  };

Finally, wire up the button to trigger connectWallet once the button is clicked.

<button
            style={{
              fontSize: "16px",
              padding: "15px",
              fontWeight: "bold",
              borderRadius: "5px",
            }}
            onClick={connectWallet}
          >
            Connect to Phantom Wallet
          </button>

Test the logic by going back to the browser and clicking on the “Connect to Phantom Wallet” button. You should see a Phantom browser extension prompting you to whether connect the app.

After clicking the button to connect to the wallet for the first time

This is a mechanism to ensure we are trusting to connect our account with any site. Click on “Connect”. This will add our app as part of the list of trusted apps.

Finding the list of trusted apps in the Phantom wallet

You can always verify the list of trusted apps in our Phantom wallet. To find them,

  1. Click on the Phantom browser extension
  2. Click on the Settings icon
  3. Click on the “Trusted Apps” option
  4. Verify our app is trusted by verifying the address, which is localhost:3000 since we are currently developing in our local environment.

Finding Phantom wallet trusted apps

Display Account and Hide “Connect to Wallet” button

Once the account is connected, it is not necessary to have available the “Connect to Phantom Wallet” button. Hence, we are going to check if the walletKey does not exist. If so, do not display the button. Also, add a check to verify the walletKey exists to display the account to the user.

{provider && !walletKey && (
          <button
            style={{
              fontSize: "16px",
              padding: "15px",
              fontWeight: "bold",
              borderRadius: "5px",
            }}
            onClick={connectWallet}
          >
            Connect to Phantom Wallet
          </button>
        )}

        {provider && walletKey && <p>Connected account {walletKey}</p>}

Refresh the app and verify the account is displayed once you connect to the wallet, and check the “Connect to wallet” button is no longer enabled.

Add button to disconnect from the Phantom Wallet

Just like when you log in to a traditional app, you want to have the option to log out. In this case, we want to connect and disconnect to a digital wallet.

To allow users to disconnect the wallet, use the disconnect() function provided by Solana. To do this, we are going to create a disconnectWallet variable and assign the following function.

/**
   * @description disconnect Phantom wallet 
   */
  const disconnectWallet = async () => {
    // @ts-ignore
    const { solana } = window;

    if (walletKey && solana) {
      await (solana as PhantomProvider).disconnect();
      setWalletKey(undefined);
    }
  };

Add a disconnect button which will trigger the disconnectWallet function.

{provider && walletKey && (
          <div>
            <p>Connected account {walletKey}</p>

            <button
              style={{
                fontSize: "16px",
                padding: "15px",
                fontWeight: "bold",
                borderRadius: "5px",
                margin: "15px auto",
              }}
              onClick={disconnectWallet}
            >
              Disconnect
            </button>
          </div>
        )}

Go to your browser and verify the account is not available after you click the “Disconnect” button.

Final Code

If for any reason something wasn’t working as expected and you are stuck trying to figure out what is going, feel free to compare it with the final code.

import { useEffect, useState } from "react";
import { PublicKey, Transaction } from "@solana/web3.js";

import "./App.css";

type DisplayEncoding = "utf8" | "hex";
type PhantomEvent = "disconnect" | "connect" | "accountChanged";
type PhantomRequestMethod =
  | "connect"
  | "disconnect"
  | "signTransaction"
  | "signAllTransactions"
  | "signMessage";

interface ConnectOpts {
  onlyIfTrusted: boolean;
}

interface PhantomProvider {
  publicKey: PublicKey | null;
  isConnected: boolean | null;
  signTransaction: (transaction: Transaction) => Promise<Transaction>;
  signAllTransactions: (transactions: Transaction[]) => Promise<Transaction[]>;
  signMessage: (
    message: Uint8Array | string,
    display?: DisplayEncoding
  ) => Promise<any>;
  connect: (opts?: Partial<ConnectOpts>) => Promise<{ publicKey: PublicKey }>;
  disconnect: () => Promise<void>;
  on: (event: PhantomEvent, handler: (args: any) => void) => void;
  request: (method: PhantomRequestMethod, params: any) => Promise<unknown>;
}

function App() {
  const [provider, setProvider] = useState<PhantomProvider | undefined>(
    undefined
  );
  const [walletKey, setWalletKey] = useState<PhantomProvider | undefined>(
    undefined
  );

  /**
   * @description gets Phantom provider, if it exists
   */
  const getProvider = (): PhantomProvider | undefined => {
    if ("solana" in window) {
      // @ts-ignore
      const provider = window.solana as any;
      if (provider.isPhantom) return provider as PhantomProvider;
    }
  };

  /**
   * @description prompts user to connect wallet if it exists
   */
  const connectWallet = async () => {
    // @ts-ignore
    const { solana } = window;

    if (solana) {
      try {
        const response = await solana.connect();
        console.log("wallet account ", response.publicKey.toString());
        setWalletKey(response.publicKey.toString());
      } catch (err) {
        // { code: 4001, message: 'User rejected the request.' }
      }
    }
  };

  /**
   * @description disconnect Phantom wallet 
   */
  const disconnectWallet = async () => {
    // @ts-ignore
    const { solana } = window;

    if (walletKey && solana) {
      await (solana as PhantomProvider).disconnect();
      setWalletKey(undefined);
    }
  };

  // detect phantom provider exists
  useEffect(() => {
    const provider = getProvider();

    if (provider) setProvider(provider);
    else setProvider(undefined);
  }, []);

  return (
    <div className="App">
      <header className="App-header">
        <h2>Tutorial: Connect to Phantom Wallet</h2>
        {provider && !walletKey && (
          <button
            style={{
              fontSize: "16px",
              padding: "15px",
              fontWeight: "bold",
              borderRadius: "5px",
            }}
            onClick={connectWallet}
          >
            Connect to Phantom Wallet
          </button>
        )}

        {provider && walletKey && (
          <div>
            <p>Connected account {walletKey}</p>

            <button
              style={{
                fontSize: "16px",
                padding: "15px",
                fontWeight: "bold",
                borderRadius: "5px",
                margin: "15px auto",
              }}
              onClick={disconnectWallet}
            >
              Disconnect
            </button>
          </div>
        )}

        {!provider && (
          <p>
            No provider found. Install{" "}
            <a href="https://phantom.app/">Phantom Browser extension</a>
          </p>
        )}

        <p>
          Built by{" "}
          <a
            href="https://twitter.com/arealesramirez"
            target="_blank"
            rel="noreferrer"
            className="twitter-link"
          >
            @arealesramirez
          </a>
        </p>
      </header>
    </div>
  );
}

export default App;

Conclusion

This tutorial explained how to connect to the Phantom wallet using the Phantom browser extension in a React app. However, after understanding how to connect and disconnect the wallet, you can use other frontend frameworks as all that you need is to have access to the solana object injected by the Phantom browser extension.

Did you like this article?

Share your thoughts by replying on Twitter of Become A Better Programmer or to my personal Twitter account.