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:
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');
});
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;
};
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;
};
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;
};
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';
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!