@tapayadot/accept-react-native
v1.16.0
Published
Accept SDK for React Native/Expo (Android)
Readme

Tapaya Accept SDK for React Native
Accept card payments on Android terminals from a React Native or Expo app.
This package is a thin wrapper over the Accept SDK for Android
(com.tapaya:accept), which in turn drives the Accept plugin app — the companion
installed on the terminal that owns the card reader and the payment UI. Every surface here
mirrors the Android SDK 1:1, so its reference documentation applies unchanged.
Full documentation: docs.tapaya.com
Requirements
- Android only. There is no iOS implementation. The package is safe to import on
iOS, but every call throws, so guard them with
Platform.OS === 'android'. minSdkVersion30 (Android 11). The config plugin raises the host app to match.- Expo SDK 54+, or a bare React Native app using
expo-modules-core. Verified against SDK 57 / React Native 0.86 on an Android 16 terminal. - A custom development build — this package contains native code and does not run in Expo Go.
Installation
npx expo install @tapayadot/accept-react-nativeAdd the config plugin to app.json:
{
"expo": {
"plugins": ["@tapayadot/accept-react-native"]
}
}Then rebuild the native project:
npx expo prebuild --clean
npx expo run:androidConfig plugin options
| Option | Default | What it does |
| --- | --- | --- |
| selfInstall | false | Lets the SDK install the plugin app itself on terminals with no app store. See Installing the plugin app. |
{
"expo": {
"plugins": [
["@tapayadot/accept-react-native", { "selfInstall": true }]
]
}
}selfInstall adds the optional com.tapaya:accept-installer artifact, which carries
REQUEST_INSTALL_PACKAGES. That permission merges into your app's manifest and Play requires
you to justify it on your listing, so leave the option off unless your terminals are
storeless. Changing it takes effect on the next npx expo prebuild.
Beyond those options the plugin only raises minSdkVersion to 30. Everything else the SDK needs — the
INTERNET, ACCESS_NETWORK_STATE, and location permissions, the <queries> entries that
make the plugin app visible on API 30+, and the activities that run a payment on a
customer-facing display — ships in the Android SDK's own manifest and merges in
automatically.
Quick start
import Accept from '@tapayadot/accept-react-native';
// 1. Initialize once, at app startup.
Accept.initialize({ isProduction: false }); // false = sandbox
// 2. Authenticate the merchant.
await Accept.auth.authenticate(merchantToken);
// 3. Make sure the plugin app is installed and its terminal is activated.
if (!Accept.plugin.isInstalled()) {
await Accept.plugin.install(); // store listing, or a direct install on a storeless terminal
}
if ((await Accept.plugin.status()) !== 'readyForPayments') {
await Accept.plugin.activateTerminal();
}
// 4. Take a payment. Amounts are in the currency's minor unit.
const result = await Accept.payments.payAsync({ amount: 1000, currency: 'USD' });
if (result.type === 'success') {
console.log(result.paymentToken, result.receiptUrl);
}Location permission
A payment requires a device location fix, and the host app must request the permission —
the SDK does not. Request it before the first payment, or it fails with
LocationPermissionRequired:
import { PermissionsAndroid } from 'react-native';
await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION);API
Everything hangs off the default export.
| Surface | Access | Purpose |
| -------- | ------------------ | ------------------------------------------------------------------------------ |
| State | Accept.state | 'idle' \| 'initialized' \| 'authenticated', plus addStateListener() |
| Auth | Accept.auth | authenticate(merchantToken) |
| SDK | Accept.sdk | version, isProduction, deviceId(), minimumAmounts() |
| Merchant | Accept.merchant | info(), config(), onboardingStatus(), availableCurrencies() |
| Payments | Accept.payments | pay(), payAsync(), status(), cancel(), refund(), setOptions() |
| Plugin | Accept.plugin | isInstalled(), install(), activateTerminal(), status(), updateInfo(), logout() |
| Displays | Accept.displays | all(), customerFacing(), isDualScreen |
Top-level: initialize(), clear(), setTheme(), getTheme(),
createLocalThemeImageUri(), setLogLevel(), setDebugLoggingEnabled(), setLogger().
Lifecycle
Accept.state moves idle → initialized → authenticated:
const subscription = Accept.addStateListener((state) => {
if (state === 'authenticated') loadMerchant();
});
// later
subscription.remove();Payments as a stream
payAsync() is a convenience over pay(), which reports the whole lifecycle. Use it when
the UI reflects the intermediate states:
const subscription = Accept.payments.pay({ amount: 1000, currency: 'USD' }, (event) => {
switch (event.type) {
case 'creating':
setStatus('Creating payment…');
break;
case 'created':
// The payment token and receipt URL exist before the plugin is launched.
setReceiptUrl(event.receiptUrl);
break;
case 'launched':
setStatus('Waiting for the card…');
break;
case 'result':
setResult(event.payResult); // success | declined | canceled | failed
break;
case 'creationFailed':
setError(event.error); // an AcceptError payload; the payment never started
break;
}
});pay() never throws — a failure to create the payment arrives as creationFailed. The
stream ends after result or creationFailed and the subscription removes itself; call
subscription.remove() to stop listening early (the plugin's UI stays on screen).
Payment options
Per payment, or session-wide via Accept.payments.setOptions():
Accept.payments.setOptions({
receiptConfig: {
type: 'show',
showQrCodeReceipt: true,
dismissBehavior: { type: 'delayed', delayMs: 5000 },
printBehavior: 'onDemand',
},
nfcPosition: { type: 'onDevice', surface: 'back', xFraction: 0.5, yFraction: 0.25 },
transactionTimeoutMs: 60_000, // 0…120000
});
await Accept.payments.payAsync({
amount: 2500,
currency: 'EUR',
tip: 250, // skips the plugin's own tip screen
metadata: { orderId: 'A-1024' }, // echoed back on the result
});Only the keys you pass are changed, so one default can be set without disturbing the others.
Dual-screen terminals
On dual-sided hardware, run the payment UI on the customer-facing panel while your app keeps the merchant panel:
if (Accept.displays.isDualScreen) {
Accept.payments.setOptions({ display: { type: 'customerFacing' } });
}Set this before authenticate() — the terminal warm-up it fires follows the same target,
and you don't want the warm-up flashing on the merchant's screen.
Installing the plugin app
Accept.plugin.install() takes whichever route the device has.
With an app store, it opens the plugin's listing — branded when the authenticated organization
configured one — and needs no merchant session to do it. PluginStoreUnavailable means the
listing could not be opened on this device.
With no store, the SDK downloads the APK and installs it through the system installer. The APK is verified against its SHA-256, declared package and version code, a pinned signer, and the installed plugin's own signer before anything is committed. That route needs three things, each of which has its own error when missing:
| Error | Missing | Fix |
| --- | --- | --- |
| PluginSelfInstallUnavailable | the installer artifact | set selfInstall: true on the config plugin and re-run npx expo prebuild |
| PluginInstallAuthRequired | an authenticated merchant | call Accept.auth.authenticate() first |
| PluginInstallPermissionRequired | the user's "Install unknown apps" grant | send them to Android's MANAGE_UNKNOWN_APP_SOURCES screen, then call install() again |
import { Linking } from 'react-native';
try {
await Accept.plugin.install();
} catch (error) {
if (!AcceptError.is(error)) throw error;
if (error.code === 'PluginInstallPermissionRequired') {
await Linking.sendIntent('android.settings.MANAGE_UNKNOWN_APP_SOURCES', [
{ key: 'android.provider.extra.APP_PACKAGE', value: 'com.yourcompany.yourapp' },
]);
}
}PluginDownloadNotPermitted is the one that is not fixed in code: the merchant is not enabled
for the non-store distribution channel. Point them at their account manager or
[email protected].
Accept.plugin.updateInfo() answers from the backend's release metadata, unauthenticated and
against the installed package, so it works during device setup, with no store, and before the
plugin is installed at all.
Errors
Every rejection is an AcceptError whose code names the failure, carrying that case's
fields:
import Accept, { AcceptError } from '@tapayadot/accept-react-native';
try {
await Accept.payments.payAsync({ amount: 10, currency: 'USD' });
} catch (error) {
if (!AcceptError.is(error)) throw error;
switch (error.code) {
case 'AmountBelowMinimum':
alert(`Minimum is ${error.minimum} ${error.currency}`);
break;
case 'PluginNotReadyForPayments':
await Accept.plugin.activateTerminal();
break;
case 'LocationPermissionRequired':
await requestLocationPermission();
break;
case 'NoInternetConnection':
alert('No connection.');
break;
default:
throw error;
}
}Theming
Branding for the plugin's activation, status, and payment screens. Session-wide, and pushed to the plugin the next time it is bound or launched:
Accept.setTheme({
light: {
colors: { primary: '#0B5FFF', onPrimary: '#FFFFFF', surface: '#FFFFFF' },
images: { logoSymbol: 'https://example.com/logo.png' },
},
dark: {
colors: { primary: '#6E9BFF', onPrimary: '#001B3D', surface: '#101418' },
},
});For a logo bundled with the app rather than hosted, convert it and hand the plugin a
content:// URL:
import { assetToBase64 } from '@tapayadot/accept-react-native/utils';
const base64 = await assetToBase64(require('./assets/logo.png'));
const uri = Accept.createLocalThemeImageUri(base64, 'png');
Accept.setTheme({ light: { images: { logoSymbol: uri } } });assetToBase64 needs expo-asset and expo-file-system, both present in managed Expo
projects. Bare React Native consumers must install them.
Logging
Accept.setLogLevel('DEBUG'); // NONE | ERROR | WARN | INFO | DEBUG | VERBOSE
Accept.setLogger((entry) => Sentry.addBreadcrumb({ message: entry.message }));Logging defaults to DEBUG against sandbox and NONE against production. Pass null to
setLogger to restore the default Logcat sink.
Logout
await Accept.plugin.logout(); // sign the merchant out of the plugin app
await Accept.clear(); // clear the local token, device id, cached config, and themeRunning the example app
example/ is a demo harness mirroring the Accept SDK for Android's own example module: one
control per public API call, and a log pane showing what each one returned.
npm install # installs the root package and the example workspace
cp example/.env.example example/.env.local # optional: prefill credentials
npm run android # prebuilds and runs on a connected device.env.local is gitignored. Leave it out and type the API key and merchant token into the app's
Credentials fields instead — nothing is persisted either way. Never put a production key in it:
Expo inlines EXPO_PUBLIC_* variables into the bundle at build time.
The example needs a real device with the Accept plugin app installed; an emulator has no card reader. Tap Login to mint a merchant token and authenticate, then activateTerminal() once before the first payment.
