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:
import { initOnRamp } from "@coinbase/cbpay-js";
let isSetupInitiated = false;
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:
if (isSetupInitiated) {
return;
}
isSetupInitiated = true;
const {
appId,
addresses,
assets,
debug = false
} = options;
let onrampInstance = null;
initOnRamp({
appId,
widgetParameters: {
addresses,
assets,
},
onSuccess: () => {
if (debug) console.log('Coinbase transaction successful');
},
onExit: () => {
if (debug) console.log('Coinbase widget closed');
},
onEvent: (event) => {
if (debug) console.log('Coinbase event:', event);
},
experienceLoggedIn: 'popup',
experienceLoggedOut: 'popup',
closeOnExit: true,
closeOnSuccess: true,
}, (_, instance) => {
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 = () => {
const button = findButtonInShadowDOM(document);
if (button && onrampInstance) {
if (debug) console.log('Found button and Coinbase instance ready');
button.classList.remove('disabled');
button.removeAttribute('disabled');
const newButton = button.cloneNode(true);
button.parentNode?.replaceChild(newButton, button);
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:
const startTime = Date.now();
const checkInterval = setInterval(() => {
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:
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 = () => {
const isLoggedIn = useIsLoggedIn();
const { primaryWallet } = useDynamicContext();
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 () => {
if (!isEthereumWallet(primaryWallet)) {
console.log('not an Eth Wallet');
return;
}
if (cleanup) {
cleanup();
}
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
});
};
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!