Introduction

If you want to embed any onramp provider in the widget, we’ve got your back! In this example we’ll integrate the Coinbase onramp but the principle is exactly the same for any other.

Pre-requisites

We assume you already have Dynamic integrated into a React based app, and are using the Dynamic Widget. The plan is to override the existing Buy button in the user profile page of the widget, so make sure you have onramps enabled in your Dynamic dashboard for that to show up.

Full Demo

You can find the full example app of this implementation code here, and the deployment at https://custom-onramp-example.vercel.app/.

Implementation

Install dependancies

The only other library we will need is the Coinbase Pay javascript SDK:

npm i @coinbase/cbpay-js

Scaffold our override file

Create a new file in your codebase called customOnramp.js. In it let’s add the imports and declarations needed to get started:

// Method to initialize the Coinbase Pay instance.
import { initOnRamp } from "@coinbase/cbpay-js";

// We want to only run things once, and this variable will help.
let isSetupInitiated = false;

// Empty function that we will fill out in the next section.
export const setupCoinbaseButtonOverride = (options) => {
}

For the following sections, unless otherwise told, place the code inside the now empty setupCoinbaseButtonOverride function.

Setup the pay instance

Inside the setupCoinbaseButtonOverride, let’s set up a few things, including the CB pay instance:

  // Stop if it already ran
  if (isSetupInitiated) {
    return;
  }
  
  // Set our flag to say the function has initiated
  isSetupInitiated = true;

  // Destructure the options needed for the onramp
  const {
    appId,
    addresses,
    assets,
    debug = false
  } = options;

  // Variable to hold the instance
  let onrampInstance = null;
  
  // Initialize the onramp
  initOnRamp({
    appId,
    widgetParameters: {
      addresses,
      assets,
    },
    // Transaction callback
    onSuccess: () => {
      if (debug) console.log('Coinbase transaction successful');
    },
    // Widget close callback
    onExit: () => {
      if (debug) console.log('Coinbase widget closed');
    },
    // General event callback
    onEvent: (event) => {
      if (debug) console.log('Coinbase event:', event);
    },
    experienceLoggedIn: 'popup',
    experienceLoggedOut: 'popup',
    closeOnExit: true,
    closeOnSuccess: true,
  }, (_, instance) => {
    // Set assign the instance to our variable
    onrampInstance = instance;
    if (debug) console.log('Coinbase instance initialized');
  });

Find the current Buy button

Add a new function (still inside setupCoinbaseButtonOverride) called findButtonInShadowDOM which is responsible for detecting the button in the shadow dom:

  const findButtonInShadowDOM = (root) => {
    const shadows = root.querySelectorAll('*');
    for (const element of shadows) {
      if (element.shadowRoot) {
        const button = element.shadowRoot.querySelector('[data-testid="buy-crypto-button"]');
        if (button) {
          return button;
        }
        const deepButton = findButtonInShadowDOM(element.shadowRoot);
        if (deepButton) {
          return deepButton;
        }
      }
    }
    return null;
  };

Override the current button

Add another new function (still inside setupCoinbaseButtonOverride) called setupOverride which is responsible replacing the existing button functionality with our own:

  const setupOverride = () => {
    // Call our previously declared function
    const button = findButtonInShadowDOM(document);
    
    // We're ready to override the button
    if (button && onrampInstance) {
      if (debug) console.log('Found button and Coinbase instance ready');
      
      // Remove disabled state
      button.classList.remove('disabled');
      button.removeAttribute('disabled');
      
      // Remove all existing click handlers
      const newButton = button.cloneNode(true);
      button.parentNode?.replaceChild(newButton, button);
      
      // Add our new click handler
      newButton.addEventListener('click', (event) => {
        event.preventDefault();
        event.stopPropagation();
        
        if (debug) console.log('Opening Coinbase widget');
        onrampInstance?.open();
      });

      return true;
    }
    
    return false;
  };

Poll for the button and onramp

Since we need both the button to be present and the onramp instance to exist for us to complete the override, we must poll for that state:

  // Set the current time 
  const startTime = Date.now();

  // Declare an interval of .5 seconds
  const checkInterval = setInterval(() => {
    
    // Run the override setup and if it succeeds or 30 seconds have passed...
    if (setupOverride() || Date.now() - startTime > 30000) {
      clearInterval(checkInterval);
      if (Date.now() - startTime > 30000) {
        if (debug) console.warn('Timeout reached while setting up Coinbase button override');
      }
    }
  }, 500);

Return a cleanup function

We need to remember to tear things down again when we’re finished:

return () => {
  clearInterval(checkInterval);
  onrampInstance?.destroy();
  isSetupInitiated = false;
};

Use setupCoinbaseButtonOverride

You’ve now finished with the setupCoinbaseButtonOverride method, so let’s add it to one of our components. It doesn’t matter to much which one as long as it’s rendered at the same time as the Widget is. Note that it cannot be the same component you declare your DynamicContextProvider in, it must be inside the component tree.

Adding Dynamic & other imports

Here we’ll do it in the same component (Main.js as we have our DynamicWidget). Let’s do the relevant imports first, we’re going to need a couple of hooks from Dynamic, as well as our setupCoinbaseButtonOverride:

// Main.js
import { DynamicWidget, useDynamicContext, useIsLoggedIn } from "@dynamic-labs/sdk-react-core";
import { isEthereumWallet } from '@dynamic-labs/ethereum';
import { setupCoinbaseButtonOverride } from './customOnramp.js';

UseEffect, Dynamic hooks & widget

Next we’ll scaffold the Main component itself and create an empty useEffect which depends on the relevant Dynamic hooks:

export default const Main = () => {

  // We need to know if user is logged in
  const isLoggedIn = useIsLoggedIn();

  // We need to know that the user has a wallet
  const { primaryWallet } = useDynamicContext();
  
  // We will fill this in next
  useEffect(() => {
  }, [isLoggedIn, primaryWallet])

  return (
    <div>
      <DynamicWidget />
    </div>
  )
};

Conditionals and calling our init

Everything from now on will be added inside the useEffect we just declared.

let cleanup;
  
const initOnramp = async () => {
  
  // We only want to support Eth here, you can do more
  if (!isEthereumWallet(primaryWallet)) {
    console.log('not an Eth Wallet');
    return;
  }

  if (cleanup) {
    cleanup();
  }

  // Fetch the currently supported EVM networks for that wallet
  const networks = primaryWallet.connector.evmNetworks.map(network => 
    network.name.toLowerCase()
  );

  cleanup = setupCoinbaseButtonOverride({
    appId: '12109858-450c-4be4-86b9-13867f0015a1',
    addresses: { 
      [primaryWallet.address]: networks
    },
    assets: ['ETH', 'USDC'],
    debug: true
  });
};

// Initialize our custom onramp
if (isLoggedIn && primaryWallet) {
  initOnramp();
}

return () => {
  if (cleanup) {
    cleanup();
  }
}

That’s it! Your Buy button now opens the coinbase onramp widget, while passing in all the relevant parameters it needs!