🤝Integration tutorial

Prerequisites

  • Node.js version 18+

  • npm version 9+

  • A text editor (for example, VS Code)

  • The TramConnectWallet extension installed

  • Basic knowledge of JavaScript and React

Steps1. Setup the project

Set up a new project using Vite, React, and TypeScript, by running the following command:

npm create vite@latest tcw-dapp-react -- --template react-ts                         

Install the node module dependencies:

cd tcw-dapp-react && npm install

Launch the development server:

npm run dev

Open the project in a text editor. To start with a blank slate, replace the code in src/App.tsx with the following:

import './App.css'

const App = () => {

    return (
        <div className="App">
            <button>Connect TramWallet</button>
        </div>
    )
}
                            
export default App

You'll need to get around type-checking by defining the window.TramStation object as any. Update src/vite-env.d.ts to the following:

/// <reference types="vite/client" />

interface Window {
  ethereum: any;
}

Also update src/App.css to the following:

.App {
    display: flex;
    flex-direction: column;
    place-items: center;
    min-width: 100vw;
    min-height: 100vh;
  }
  button {
    margin-top: 0.5em;
  }

At this point, you have a working React app with some basic styling and a button that you'll use to connect to TramConnectWallet.

2. Detect TramConnectWalletNext, detect the injected provider that browser extension wallets use. TramConnectWallet injects a JavaScript TramStation provider into the browser at window.TramStation. You will use this provider in your dapp to request user information from TramConnectWallet.Add code to conditionally render a Connect TramConnectWallet button in your component by updating src/App.tsx to the following:

import './App.css'
let injectedProvider = false

if (typeof window.TramStation !== 'undefined') {
  injectedProvider = true
  console.log(window.TramStation)
}

const isTramWallet = injectedProvider ? window.TramStation : false

const App = () => {   

  return (
    <div className="App">
      <h2>Injected Provider { injectedProvider ? 'DOES' : 'DOES NOT'} Exist</h2>
      { isTramWallet && 
        <button>Connect TramWallet</button>
      }
    </div>
  )
}

export default App

If you have the extension installed and enabled, you'll see the Connect TramConnectWallet button and the text displays Injected Provider DOES Exist.You'll also see the TramStation provider printed to the console.

You can switch between these two states by enabling or disabling the TramConnectWallet extension through your browser's Manage Extensions menu.3. Connect to TramConnectWalletTo connect your dapp to TramConnectWallet, you'll create another useState named wallet, which keeps your dapp up to date with various TramConnectWallet wallet properties such as accounts, balance, and chainId. These are essential properties to sync with your dapp constantly.You'll first add a state for accounts and slowly build up your state over the following few tutorial sections.You'll also set up a button to connect to the TramConnectWallet wallet.Update the src/App.tsx to the following:

import './App.css'
import { useState, useEffect } from 'react'

const App = () => {
  const [hasProvider, setHasProvider] = useState<boolean | null>(null)
  const initialState = { accounts: [] }                            /* New */
  const [wallet, setWallet] = useState(initialState)  /* New */

  useEffect(() => {
    const getProvider = async () => {
        let injectedProvider = false
        if (typeof window.TramStation !== 'undefined') {
            injectedProvider = true
            console.log(window.TramStation)
        }
        
        setHasProvider(Boolean(injectedProvider))
    }

    getProvider()
  }, [])

  const updateWallet = async (accounts:any) => {     /* New */
    setWallet({ accounts })                             /* New */
  }                                                  /* New */

  const handleConnect = async () => {                /* New */
    console.log("called")
    const accounts = await window.TramStation.request({   /* New */
      method: "eth_requestAccounts",                 /* New */
    })                                               /* New */
    updateWallet(accounts)                           /* New */
  }                                                  /* New */

  return (
    <div className="App">
      <div>Injected Provider {hasProvider ? 'DOES' : 'DOES NOT'} Exist</div>

      { hasProvider &&                               /* Updated */
        <button onClick={handleConnect}>Connect TramWallet</button>
      }
      
      { wallet.accounts.length > 0 &&                /* New */
        <div>Wallet Accounts: { wallet.accounts[0] }</div>
      }
    </div>
  )
}

export default App

The comments in the code snippet indicate any new or updated lines of code. The changes include:

  • Lines 6-7: Create an object representing the initial empty state and a new useState hook to reflect your TramConnectWallet wallet state.

  • Lines 23-25: Add an updateWallet function that sets your new wallet state when you connect. This will also be called when you add code later that refreshes our wallet state. This function will be helpful as you start syncing the balance and chainId.

  • Lines 27-33: Add a handleConnect function that the UI calls to connect to TramConnectWallet using window.TramStation.request and the eth_requestAccounts RPC method. Your dapp stores the result from this RPC call in a variable named accounts and passes it to the updateWallet function.

  • Lines 39-41: On click, the Connect TramConnectWallet button calls handleConnect, from which an RPC call of eth_requestAccounts is awaited, and the user is prompted to connect to TramConnectWallet:

4. Handle State Change to TramConnectWalletWith the current code, your dapp loses the account data if you refresh the page. When you connect using the button, the dapp sets accounts in its state, but in the case of a browser refresh, you need something in useEffect to check if you've already connected and update the wallet state.Thinking ahead, once you track more than just accounts, you also need a mechanism to get the balance and chainId and update their state.Update src/App.tsx with some added logic to useEffect:

import './App.css'
import { useState, useEffect } from 'react'

const App = () => {
  const [hasProvider, setHasProvider] = useState<boolean | null>(null)
  const initialState = { accounts: [] }               /* New */
  const [wallet, setWallet] = useState(initialState)  /* New */

  useEffect(() => {
    const refreshAccounts = (accounts: any) => {                /* New */
      if (accounts.length > 0) {                                /* New */
        updateWallet(accounts)                                  /* New */
      } else {                                                  /* New */
        // if length 0, user is disconnected                    /* New */
        setWallet(initialState)                                 /* New */
      }                                                         /* New */
    }     

    
    const getProvider = async () => {
        let injectedProvider = false
        if (typeof window.TramStation !== 'undefined') {
          injectedProvider = true
          console.log(window.TramStation)
        }
        setHasProvider(Boolean(injectedProvider))
        if (injectedProvider) {                                           /* New */
        const accounts = await window.TramStation.request(         /* New */
          { method: 'eth_accounts' }                            /* New */
        )                                                       /* New */
        refreshAccounts(accounts)                               /* New */
        window.TramStation.on('accountsChanged', refreshAccounts)  /* New */
      }                                                         /* New */
    }

    getProvider()
    return () => {                                              /* New */
      window.TramStation?.removeListener('accountsChanged', refreshAccounts)
    }   
  }, [])

  const updateWallet = async (accounts:any) => {     /* New */
    setWallet({ accounts })                          /* New */
  }                                                  /* New */

  const handleConnect = async () => {                /* New */
    const accounts = await window.TramStation.request({   /* New */
      method: "eth_requestAccounts",                 /* New */
    })                                               /* New */
    updateWallet(accounts)                           /* New */
  }                                                  /* New */

  return (
    <div className="App">
      <div>Injected Provider {hasProvider ? 'DOES' : 'DOES NOT'} Exist</div>

      { window.TramStation && wallet.accounts.length < 1 &&                               /* Updated */
        <button onClick={handleConnect}>Connect TramWallet</button>
      }
      
      { wallet.accounts.length > 0 &&                /* New */
        <div>Wallet Accounts: { wallet.accounts[0] }</div>
      }
    </div>
  )
}

export default App

Note that useEffect is a side effect; you use the hooks for fetching data, reading and writing to local storage, and setting up event listeners or subscriptions. The side effect occurs on the first render only since you have nothing in your dependency array. You also need to clean up those listeners upon unmount of your component. You can now test your dapp and see that when you refresh the page, you retain the display of the user's address. You can also disable the TramConnectWallet browser extension, enable it again, and reconnect to it. You will see that the React dapp has retained the user address. You've synced with a source outside your dapp and managed the state in a single component.

Last updated