@yodlpay/react-native
v0.11.0
Published
Yodl React Native SDK — drop-in components for EIP-1193 wallets
Readme
@yodlpay/react-native
Let your users pay real-world merchants with crypto, straight from your React Native app. Scan a local QR, render one component, settle on-chain — Yodl turns stablecoins into the merchant's local currency.
Drop-in crypto payments for React Native. You bring an EIP-1193 wallet provider; the SDK renders the Yodl payment UI in a hardened, origin-locked WebView and handles the RPC bridge and on-chain execution. Users keep self-custody and often settle at FX rates that beat Visa/Mastercard.
The provider interface is always plain EIP-1193. Transaction batching and paymasters ride on top via EIP-5792 (wallet_sendCalls) — available when your wallet supports EIP-5792 natively, or through the optional EIP-7702 add-on for wallets that don't.
import { createYodlSdk, YodlProvider, YodlPayment } from '@yodlpay/react-native';
const sdk = createYodlSdk({
integrationAddress: '0x000000000000000000000000000000000000dEaD',
supportedChainIds: [1, 8453],
});
// provider, address, and chainId are the live wallet state from your wallet SDK
<YodlProvider sdk={sdk} provider={walletProvider} address={address} chainId={chainId}>
<YodlPayment qrData={scannedQr} onTransactionSent={(tx) => console.log(tx)} />
</YodlProvider>;Contents
- Requirements
- Installation
- Quick Start
- Why Yodl
- Features
- Platform Notes
- Usage
- API Reference
- Security
- Troubleshooting
- License
Requirements
| Requirement | Version |
| ------------- | ----------------- |
| React | >= 18 |
| React Native | >= 0.72 |
| Hermes engine | Enabled (default) |
Uses native modules (
react-native-webview), so it requires a custom dev build — it does not run in Expo Go. See Platform Notes.
Installation
Install the SDK and its react-native-webview peer dependency, then rebuild the native app.
Expo (resolves the WebView version to your Expo SDK):
npx expo install @yodlpay/react-native react-native-webview
npx expo run:ios # or: npx expo run:androidBare React Native:
npm install @yodlpay/react-native react-native-webview # or: pnpm add / yarn add
npx pod-install # iOSThen rebuild the app. That's the full install.
Quick Start
Three steps: create the SDK once, wrap your screen in <YodlProvider>, render <YodlPayment>.
import { createYodlSdk, isPaymentQr, YodlProvider, YodlPayment } from '@yodlpay/react-native';
// 1. Create the SDK once, app-wide.
const sdk = createYodlSdk({
integrationAddress: '0x000000000000000000000000000000000000dEaD', // your integration wallet address
supportedChainIds: [1, 8453],
});
// provider, address, and chainId are the live wallet state from your wallet SDK's hooks.
export function PaymentScreen({ qrData, provider, address, chainId }) {
// 2. Bail out early if the scanned QR isn't a Yodl payment (no SDK instance needed).
if (!isPaymentQr(qrData)) return null;
// 3. Supply live wallet state; the SDK keeps the Yodl UI in sync with it.
return (
<YodlProvider sdk={sdk} provider={provider} address={address} chainId={chainId}>
<YodlPayment
qrData={qrData}
onTransactionSent={(txHash) => console.log('paid:', txHash)}
onError={(err) => console.error(err)}
/>
</YodlProvider>
);
}provider, address, and chainId all come from your wallet SDK (Privy, MetaMask, Coinbase Wallet, WalletConnect, …) — chainId is the chain the wallet is currently on, not a fixed value. See <YodlProvider /> for how this state drives the UI, and the full example for scanning, the dashboard, and lifecycle events.
Why Yodl
Pay like a local, with crypto. Live in Argentina, Brazil, the Philippines, and Vietnam — across Pix, Mercado Pago, QRPh, and VietQR.
- Real-world spending — users pay merchants by scanning everyday local QR codes; merchants get their own currency, never crypto.
- Self-custody — funds stay in the user's wallet until the moment of payment. No deposits, no custodian.
- Beats card FX — on-chain settlement regularly comes in cheaper than Visa/Mastercard abroad.
- Ship in an afternoon — one component, no payment UI or RPC plumbing to build.
Features
- Wallet-agnostic — any EIP-1193 provider (Privy, MetaMask, Coinbase Wallet, Rainbow, WalletConnect).
- Three drop-in components —
<YodlSignup>for onboarding,<YodlPayment>for QR payments,<YodlDashboard>for the account view. - Hardened by default — UI locked to the Yodl origin, off-origin navigation blocked. No config.
- Batching & paymasters — bridges EIP-5792 (
wallet_sendCalls); native support passes through, or add the EIP-7702 provider for atomic batching + gas sponsorship. - Multi-chain — Ethereum, Arbitrum, Base, Polygon, Optimism, BNB Chain, Gnosis.
- Fully typed — first-class TypeScript throughout.
Platform Notes
Most apps just need the install above and a native rebuild. Edge cases:
- Not Expo Go — uses the native
react-native-webview, so run a dev build (npx expo run:ios/run:android) or bare React Native. - Hermes required — the SDK uses
BigInt(Hermes is on by default in RN0.70+). If you disabled Hermes, re-enable it or add aBigIntpolyfill.
Usage
Create the SDK once with createYodlSdk(config) and supply wallet state at runtime via <YodlProvider>. Stateless helpers like isPaymentQr(qrData) are standalone exports — no SDK instance required to call them.
Signup flow
Render <YodlSignup /> when you need the hosted onboarding flow. The hosted Yodl UI owns the status check, SIWE, email capture, verification code entry, and the final signed-up screen.
import { useState } from 'react';
import { Text } from 'react-native';
import { YodlProvider, YodlSignup } from '@yodlpay/react-native';
import { sdk } from './sdk';
import { useWallet } from './wallet';
export function SignupScreen() {
const { provider, address, chainId, isConnected } = useWallet();
const [showSignup, setShowSignup] = useState(true);
if (!isConnected) return <Text>Connect a wallet first.</Text>;
if (!showSignup) return null;
return (
<YodlProvider sdk={sdk} provider={provider} address={address} chainId={chainId}>
<YodlSignup
onStatusChange={(status) => console.log('signup status:', status)}
onSignedUp={() => {
// Store completion however your app tracks user state.
console.log('signed up');
setShowSignup(false);
}}
onDismiss={() => setShowSignup(false)}
onError={console.error}
/>
</YodlProvider>
);
}<YodlSignup /> shows the Yodl signup flow and calls onSignedUp when the user is finished. The SDK does not expose account details from the hosted flow. If you need to remember that the user signed up, save completion in your own app state, storage, or backend.
Hosted UI theming
Theming is opt-in. Set theme, themeMode, or both on <YodlProvider> (or on any hosted component) to style the Yodl UI. Omit them and the hosted UI keeps its own appearance. Every field is optional.
import type { YodlTheme } from '@yodlpay/react-native';
import { YodlPayment, YodlProvider } from '@yodlpay/react-native';
// A small, representative subset of tokens — see "Tokens" for the full list.
const brandTheme: YodlTheme = {
name: 'acme',
light: { bg: '#ffffff', surface: '#f4f4f5', text: '#18181b', accent: '#6d4fc0' },
dark: { bg: '#18181b', surface: '#27272a', text: '#fafafa', accent: '#9662ff' },
images: {
light: { loading: 'https://cdn.example.com/yodl-loading-light.mp4' },
dark: { loading: 'https://cdn.example.com/yodl-loading-dark.mp4' },
},
};
<YodlProvider
sdk={sdk}
provider={provider}
address={address}
chainId={chainId}
theme={brandTheme}
themeMode="system"
>
<YodlPayment qrData={qrData} />
</YodlProvider>;Modes
themeMode is 'system' (follow the device), 'light', or 'dark'. The two props combine as:
| theme | themeMode | Result |
| --- | --- | --- |
| Omitted | Omitted | Hosted UI keeps its own appearance |
| Set | Omitted | Your palette, following the device |
| Omitted | Set | Stock Yodl palette, pinned to that mode |
| Set | Set | Your palette, pinned to that mode |
Defaults and overrides
theme/themeModeon<YodlProvider>are defaults for every hosted component under it.- The same props on
<YodlSignup>,<YodlPayment>, and<YodlDashboard>override the provider per prop — set one and the other still inherits. - Remove
themeandthemeModefrom both the component and its provider to hand appearance control back to the hosted UI.
// Provider follows the device; this one screen pins dark but keeps the palette.
<YodlProvider sdk={sdk} provider={provider} address={address} chainId={chainId} theme={brandTheme}>
<YodlPayment qrData={qrData} themeMode="dark" />
</YodlProvider>;Tokens
name is an optional label (≤64 chars). Set colors per mode under light and dark; omitted tokens keep their stock value. The 21 keys:
- Core:
bgsurfacesurfaceSecondarytexttextSecondarytextMutedbordersuccesserrorwarningaccent - Fine-tune (derived from core when omitted):
bgDeepsurfaceHoverskeletonaccentStrongaccentDeep - Brand:
brandSurfacebrandSheetbrandTextbrandButtonbrandButtonText
Light accents are handled for you: icons drawn on an accent fill use white by default, and when white would drop below the 3:1 non-text contrast floor — a lime accent tests at 1.7:1 — near-black ink is substituted automatically. An accent white already reads on is left untouched. Where the accent is used as text it is mixed 45% toward text for the same reason.
Set brandSurface (or brandSheet) alone and readable text/button colors are picked from its luminance — a light brand surface gets near-black text and a dark button.
Shape and typography
font, radiusControl and radiusSurface are top-level keys, not palette keys — none is a color and none varies by mode.
const brandTheme: YodlTheme = {
font: 'geist',
radiusControl: '12px',
light: { bg: '#ffffff' },
};font is one of 'inter' (stock), 'geist', 'mono', or 'system'. It is a fixed list rather than a family name you supply, because a font bundled in your app is not visible to the WebView that hosts the Yodl UI — naming it would silently fall back to Inter and look like a bug. Ask us if you need a face that isn't listed.
radiusControl is the corner radius of every button, input and badge; stock is 9999px, a full pill. Cards, list rows and sheets follow it in both directions — sharper or rounder than stock — with the rest of the surface scale derived proportionally:
| radiusControl | Buttons | Cards | Sheets |
| --- | --- | --- | --- |
| '0' | square | 0 | 0 |
| '12px' | 12px | 12px | 18px |
| '32px' | 32px | 32px | 48px |
| omitted (9999px) | pill | 16px | 24px |
The exception is a pill-scale value — 100px and up, or 50% and up (a smaller percentage like '6%' is an ordinary radius and surfaces follow it): that asks for a shape rather than a size, so buttons go pill while surfaces stay at their stock radius instead of becoming lozenges. Set radiusSurface when you want containers to differ from your buttons — pill buttons with square cards, or the very round cards a pill radiusControl won't produce on its own.
Circular elements — avatars, token icons, sheet grab handles, status rings — stay circular by design and follow neither key.
Validation
- Colors must be
#rgb,#rgba,#rrggbb,#rrggbbaa,rgb(…), orrgba(…). fontmust be one of the four listed values; anything else rejects the payload.radiusControl/radiusSurfacemust be a plain CSS length — a bare0, or a number of up to 4 digits withpx,rem,em, or%(.5remis as valid as0.5rem). A unitless number other than zero ('4'rather than'4px') is not a CSS length and is rejected, as are expressions such ascalc(…)andvar(…).- A single invalid key rejects the whole theme, so the hosted UI falls back to stock rather than applying a half-valid palette. Validate with
safeParseYodlTheme()before sending — it returns the offending key by name. - Images go under
images.light/images.dark, each withpayBackground,loading, andprocessingslots.loadingandprocessingare one visual slot: the hosted UI has a single splash state and rendersloading, falling back toprocessingonly whenloadingis unset — settingloadingalone covers everything, and there is no need to set both. A video URL (.mp4,.m4v,.webm,.mov) plays as a looping muted video; anything else renders as an image, so every browser-supported image format works — including animated WebP, APNG, animated AVIF, and GIF..ogvis excluded on purpose — iOS WebViews cannot decode Ogg/Theora, so it would blank the splash there. - The
loadingsplash renders above the stepped caption, in normal flow where the stock Yodl mark sits — not over the whole viewport. The media keeps its own aspect ratio and is scaled proportionally within maximum dimensions; it is not placed into a container of a set size. The maxima are 16rem wide, no more than 70% of the viewport width, and no more than 40% of its height. Width is applied first and the height follows from the asset's own ratio, so a wide asset occupies a shorter band than a square one and the vertical space taken varies by shape. Source pixel dimensions cannot bypass the maxima: a 4000px-wide asset is scaled down to the same width as a 400px-wide one of the same shape, only sharper. The fit iscontain, so the media is only ever scaled down to stay within the maxima — never cropped to fill the viewport the way a background image would be — and the whole asset stays visible. Images and videos behave identically. Author roughly square (around 512×512) for predictable scaling, keep important detail away from the edges, and usepayBackgroundfor a full-screen backdrop. - Image URLs must be
https://, at most 2048 characters, with no whitespace, quotes, or backslashes. - An invalid
themeorthemeModenever crashes — it falls back to stock Yodl and logs the error in__DEV__.
Live updates
Changing theme / themeMode on a mounted component applies over the bridge without reloading — the active flow keeps its state.
Compatibility.
yodlUiBaseUrlmust point at a current hosted UI that supports correlated readiness. Against an older build the theme is never delivered, and the WebView is only revealed by the 8 s ceiling — showing stock Yodl colors rather than yours. See Loading and readiness and Troubleshooting.
Full example
sdk.ts — create the instance once, app-wide:
import { createYodlSdk } from '@yodlpay/react-native';
export const sdk = createYodlSdk({
integrationAddress: '0x000000000000000000000000000000000000dEaD',
// yodlUiBaseUrl defaults to https://sdk-webview.yodl.me
supportedChainIds: [1, 42161, 8453, 137, 10, 56, 100],
});PaymentScreen.tsx — scan → pay → back, plus the dashboard:
import { useState } from 'react';
import { Button, Text, View } from 'react-native';
import { isPaymentQr, YodlProvider, YodlPayment, YodlDashboard } from '@yodlpay/react-native';
import { sdk } from './sdk';
import { useWallet } from './wallet'; // your wallet SDK (Privy, WalletConnect, …)
import { QrScanner } from './QrScanner'; // your camera component, e.g. expo-camera
type Screen = 'scan' | 'pay' | 'dashboard';
export function PaymentScreen() {
// `provider` is your EIP-1193 wallet. If it already supports EIP-5792 natively,
// pass it as-is; if not, wrap it with withYodl7702(...) for batching/paymasters (see below).
// `address`/`chainId` track the wallet.
const { provider, address, chainId, isConnected } = useWallet();
const [screen, setScreen] = useState<Screen>('scan');
const [qrData, setQrData] = useState<string | null>(null);
if (!isConnected) {
return <Text>Connect a wallet to continue.</Text>;
}
function handleScan(scanned: string) {
// Guard before rendering — no SDK instance needed for the check.
if (!isPaymentQr(scanned)) return; // not a Yodl QR: keep scanning
setQrData(scanned);
setScreen('pay');
}
return (
<YodlProvider sdk={sdk} provider={provider} address={address} chainId={chainId}>
{screen === 'scan' && (
<View style={{ flex: 1, gap: 12 }}>
<QrScanner onScan={handleScan} />
<Button title="Open dashboard" onPress={() => setScreen('dashboard')} />
</View>
)}
{screen === 'pay' && qrData && (
<YodlPayment
qrData={qrData}
onTransactionSent={(txHash) => console.log('submitted:', txHash)}
onPaymentDetails={(details) => {
// 'submitted' | 'processing' | 'success' | 'failure'
if (details.status === 'success') setScreen('scan'); // done → scan again
}}
onError={(err) => {
console.error(err);
setScreen('scan');
}}
/>
)}
{screen === 'dashboard' && <YodlDashboard onError={console.error} />}
</YodlProvider>
);
}Optional EIP-7702 fallback
Native wallet support is always preferred — if your wallet handles EIP-5792 (wallet_sendCalls) itself, pass it straight through and skip this section. @yodlpay/react-native forwards those calls natively whenever the wallet supports them. The add-on below is purely a fallback for wallets that don't.
Add @yodlpay/react-native-eip-7702-provider only when the wallet lacks the atomic EIP-5792 wallet_sendCalls support your flow requires, can sign EIP-7702 authorizations, and you want Yodl to provide that fallback path. The add-on wraps the same provider and advertises atomic batching (atomic: ready) plus paymaster support (paymasterService) through wallet_getCapabilities — so batched, gas-sponsored calls work even on wallets without native EIP-5792, while native wallet_sendCalls still wins when the wallet supports it.
import { signAuthorizationViaProvider, withYodl7702 } from '@yodlpay/react-native-eip-7702-provider';
import { YodlProvider, YodlPayment } from '@yodlpay/react-native';
// Bring your own RPC + bundler — the add-on ships no defaults, so 7702 traffic
// runs through your endpoints. List only the chains you can actually serve
// (independent of the SDK's `supportedChainIds`).
const provider = withYodl7702(walletProvider, {
chains: [
{
id: 8453,
rpcUrl: 'https://your-rpc.example/8453',
bundlerUrl: 'https://api.pimlico.io/v2/8453/rpc?apikey=YOUR_KEY',
},
],
// Required: you price each fallback UserOperation (the add-on stays bundler-
// agnostic). See the add-on's "Gas pricing" section for caching guidance.
estimateUserOperationFees: async (chainId) => getYourBundlerGasPrice(chainId),
// Required: you sign the EIP-7702 authorization (no universal wallet method).
// The add-on ships `signAuthorizationViaProvider` to try for EIP-1193 wallets.
signAuthorization: signAuthorizationViaProvider(walletProvider),
});
<YodlProvider sdk={sdk} provider={provider} address={address} chainId={chainId}>
<YodlPayment qrData={qrData} />
</YodlProvider>;Base SDK users can skip this entirely. Without it, a wallet that lacks native EIP-5792 wallet_sendCalls simply can't batch — the base SDK forwards the request and the wallet's own response (it performs no sequential fallback of its own).
API Reference
createYodlSdk(config)
Returns a YodlSdk instance.
| Option | Type | Required | Default | Description |
| ------------------- | ----------------------------- | -------- | ----------------------------- | -------------------------------------------------------------------------- |
| integrationAddress | string | Yes | — | Your integration wallet address. Identifies you to Yodl and attributes payments on chain |
| supportedChainIds | number[] | Yes | — | Chain IDs the wallet can switch to |
| yodlUiBaseUrl | string | No | https://sdk-webview.yodl.me | Base URL for the Yodl UI |
integrationAddress must be a non-empty 0x address. The SDK sends it with package metadata on Yodl-owned requests:
X-Yodl-Integration-Id: 0x000000000000000000000000000000000000dEaD
X-Yodl-SDK-Name: @yodlpay/react-native
X-Yodl-SDK-Version: <installed package version>Instance members:
sdk.config— the resolved config (read-only).
isPaymentQr(qrData)
Standalone helper — string | string[] → boolean. Returns true if any entry is a Yodl-supported payment QR. Pure: it takes no SDK instance, so you can guard a render before <YodlProvider> is mounted.
import { isPaymentQr } from '@yodlpay/react-native';
if (!isPaymentQr(scannedQr)) return null;<YodlProvider />
Supplies the SDK instance and wallet state to the components. Render it once, high in your tree.
| Prop | Type | Required | Description |
| ---------- | ----------------- | -------- | ------------------------------------- |
| sdk | YodlSdk | Yes | Instance from createYodlSdk() |
| provider | EIP1193Provider | Yes | EIP-1193 provider from any wallet SDK (@yodlpay/sdk-core's type: request only, events not required; withYodl7702 accepts and returns the same type, so no cast is needed). Prefer the wallet's native EIP-5792 support; only if it lacks it, wrap with withYodl7702(...) for the EIP-7702 fallback |
| address | string | Yes | Connected wallet address |
| chainId | number | Yes | Chain ID the wallet is currently on |
| theme | YodlTheme | No | Default custom hosted-UI palette; all fields are optional |
| themeMode | 'system' \| 'light' \| 'dark' | No | Default host-controlled mode |
address and chainId are the wallet's live account and chain — pass them straight from your wallet SDK's hooks (e.g. useWallets(), useAccount()), not as fixed values. They tell the embedded Yodl UI who is connected and on which chain, and the SDK pushes accountsChanged / chainChanged into the UI whenever they change — so keep them updated when the user switches account or network. (The provider is used only as the RPC/signing transport.)
theme and themeMode are also available on <YodlSignup>, <YodlPayment>, and <YodlDashboard> as per-component overrides — along with backgroundColor and style. See Shared visual props and Hosted UI theming.
Shared visual props
<YodlSignup>, <YodlPayment>, and <YodlDashboard> all accept the same visual props. They are listed here once rather than repeated in each component's table below.
| Prop | Type | Required | Description |
| ----------------- | ------------------------------- | -------- | --------------------------------------------------------------------------------- |
| theme | YodlTheme | No | Palette override; inherits the provider theme when omitted |
| themeMode | 'system' \| 'light' \| 'dark' | No | Mode override; inherits the provider themeMode when omitted |
| backgroundColor | string | No | Color behind the WebView before first paint; overrides the theme background |
| renderLoading | () => ReactElement \| null | No | Replaces the loading overlay's contents; null suppresses the overlay entirely |
| onReady | () => void | No | Called once per mount, the first time the hosted UI is revealed |
| style | ViewStyle | No | Container style |
See Loading and readiness for renderLoading and onReady.
<YodlSignup />
Renders the hosted signup flow. Use this as a full-screen component inside <YodlProvider>. Also accepts all shared visual props.
| Prop | Type | Required | Description |
| ---------------- | ---------------------------------------------------------------------------- | -------- | ----------------------------------------------------- |
| onStatusChange | (status: 'checking' \| 'signed_out' \| 'signing_up' \| 'signed_up') => void | No | Called when the hosted flow reports signup status |
| onSignedUp | () => void | No | Called when signup completes |
| onDismiss | () => void | No | Called when the hosted success screen is dismissed |
| onError | (error: Error) => void | No | Called on SIWE, registration, verification, or RPC errors |
<YodlPayment />
Renders the payment flow for a scanned QR code. Renders nothing if qrData is empty or unsupported. Also accepts all shared visual props.
| Prop | Type | Required | Description |
| ------------------- | --------------------------------------- | -------- | -------------------------------------------------- |
| qrData | string | Yes | Raw scanned QR data string |
| navigateOnSubmit | 'always' \| 'never' | No | Whether the hosted UI moves to its own status screen once the transaction is submitted. Defaults to 'always' |
| onTransactionSent | (txHash: string) => void | No | Called after a transaction is sent |
| onPaymentDetails | (details: YodlPaymentDetails) => void | No | Called with payment lifecycle details from the UI |
| onError | (error: Error) => void | No | Called on RPC / payment errors |
navigateOnSubmit="never" parks the hosted UI on a terminal "sent" frame so your app owns the transition after signing — drive it from onTransactionSent or onPaymentDetails. Note the moment: the hosted UI moves when the transaction is submitted, not when it settles. It keeps polling behind that frame, so onPaymentDetails still reports 'processing' and then 'success' or 'failure' — you get a 'submitted' message first, then the same stream 'always' produces. A host that ignores both callbacks leaves the user on that frame, which is why the default is 'always': omit the prop and the URL, and therefore the behavior, is unchanged. The prop feeds the WebView's URL and the WebView is keyed by it, so changing it on a mounted component reloads the hosted UI — pass a constant.
<YodlDashboard />
Renders the full Yodl account dashboard. Also accepts all shared visual props.
| Prop | Type | Required | Description |
| ------------------- | --------------------------------------- | -------- | -------------------------------------------------- |
| onTransactionSent | (txHash: string) => void | No | Called after a transaction is sent |
| onPaymentDetails | (details: YodlPaymentDetails) => void | No | Called with payment lifecycle details from the UI |
| onError | (error: Error) => void | No | Called on RPC errors |
Loading and readiness
Until the hosted UI reports it has painted, the SDK covers the WebView with an overlay filled with backgroundColor and centered on an ActivityIndicator. That exists so the user never sees react-native-webview's hardcoded-white loading view or the document's default white before its CSS lands.
renderLoading and onReady are two ways to fix the two-loader cascade you get when your app's loading treatment and Yodl's don't match. They are alternatives — pick one. Using both is how you get two loaders.
Recipe A — Yodl's overlay, your loader. The SDK still owns the cover; you supply its contents. Nothing in your tree needs to know when the hosted UI is up, so onReady has no job here.
<YodlPayment
qrData={qrData}
backgroundColor="#18181b"
renderLoading={() => <MyBrandLoader />}
/>- Returning an element renders it centered on
backgroundColor. The wrapper stays, so the no-white-flash guarantee is preserved. - Returning
nullsuppresses the overlay entirely — no view is mounted, so nothing is left over the WebView intercepting touches.backgroundColoris still painted behind the WebView. - It is called on every render while loading, so keep it cheap.
Recipe B — your own screen covers it. Suppress Yodl's overlay so there is only one, and let onReady tell you when to take yours down.
{!ready && <MyLoadingScreen />}
<YodlPayment
qrData={qrData}
backgroundColor="#18181b"
renderLoading={() => null}
onReady={() => setReady(true)}
/>onReady is the only way to do this: renderLoading is called during render, so you cannot setState from it.
onReady fires once per mount, the first time the UI is revealed. It does not fire again. The hosted document re-posts readiness when it reloads, but the overlay has already lifted and does not come back, so there is nothing to re-announce — it lifts once and stays lifted.
The reveal has a ceiling. If nothing is heard from the hosted UI within 8 seconds, the SDK reveals anyway and fires onReady. It is a last resort for a page that is genuinely dead, not a routine path: a reveal on that timer arrives without the readiness handshake, so the hosted UI has not received your theme or the SDK's capability announcement. A current hosted build runs its own, shorter ceiling and always reports readiness first, so in practice you only reach 8 seconds when the UI never loaded at all. The ceiling reveals — it does not report an error. Use onError and your own network handling for that.
Payment details events
onPaymentDetails is optional and additive — onTransactionSent(txHash) and onError(error) behavior is unchanged.
The Yodl UI emits generalized payment lifecycle details once it has payment status. The SDK forwards them to onPaymentDetails. Normalized fields cover transaction hash, chain, lifecycle status, recipient, invoice, token amounts, refund state, and rewards when available; the original payload may be included under raw. The SDK does not fetch transaction details itself — it only bridges UI messages.
Security
<YodlSignup>, <YodlPayment>, and <YodlDashboard> inject an EIP-1193 provider bound to the user's wallet, so the underlying Yodl UI is locked to the configured Yodl origin and off-origin navigation is blocked (onShouldStartLoadWithRequest). This prevents origin spoofing and mid-request navigation that could intercept signed responses. The injected provider also enforces a 30 s pending-request timeout.
The bridge forwards EIP-5792 (wallet_sendCalls, wallet_getCallsStatus, wallet_showCallsStatus, wallet_getCapabilities) and all signing methods straight to the supplied wallet provider — it owns no smart-account stack and makes no RPC calls of its own (the embedded UI reads chain state through its own provider). For wallets without native EIP-5792, add @yodlpay/react-native-eip-7702-provider for atomic batching.
Troubleshooting
| Symptom | Cause & fix |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Can't find variable: BigInt | Hermes is disabled. Re-enable Hermes or add a BigInt polyfill. See Platform Notes. |
| App crashes / blank screen in Expo Go | Native modules aren't available in Expo Go. Use a development build. |
| Yodl components must be rendered inside <YodlProvider> | A component is mounted outside the provider. Wrap it in <YodlProvider>. |
| Payment screen renders blank | qrData isn't a Yodl payment QR. Guard with isPaymentQr(qrData) before rendering <YodlPayment>. |
| webview-action providers are not supported yet | The scanned QR maps to a provider whose hosted webview flow isn't supported yet. Catch it (e.g. via onError or an error boundary) and prompt a retry. |
| Overlay lifts after ~8 s onto an unthemed or blank page | That's the reveal ceiling firing because the hosted UI never reported readiness — usually an unreachable yodlUiBaseUrl, a build that predates correlated readiness, or no network. A ceiling reveal carries no theme handshake, so a stale build also shows stock Yodl colors. Point yodlUiBaseUrl at a current hosted build. See Loading and readiness. |
| Nothing is visible over the WebView while it loads | renderLoading returned null, which suppresses the overlay by design. Return an element instead, or omit the prop for the default indicator. |
License
BUSL-1.1 © Yodl
