This guide documents what mobile developers need to know before integrating the PocketPay SDK into a React Native or Expo application. It covers known limitations, required polyfills, storage recommendations, Metro bundler configuration, environment variable handling, and a working integration example.
Caution
The SDK is primarily developed and tested against Node.js ≥ 18. React
Native and Expo support is untested in this SDK's own CI. The guidance
here reflects community best-practice and the known runtime requirements of
@stellar/stellar-sdk, but you are responsible for validating behaviour in
your specific environment and version combination before shipping to users.
- Expo vs Bare React Native
- Compatibility Summary
- Required Polyfills
- Metro Bundler Configuration
- Secure Key Storage
- Environment Configuration (no dotenv)
- Soroban Vault Considerations
- Integration Example
- Known Limitations
- Related Docs
The SDK can be used in both Expo-managed and bare React Native projects, but the setup steps differ slightly:
| Area | Expo (managed workflow) | Bare React Native |
|---|---|---|
| Polyfill entry point | index.js or top of your root component |
index.js before any SDK import |
| Crypto | expo-crypto + react-native-get-random-values |
react-native-get-random-values |
| Secure storage | expo-secure-store |
react-native-keychain |
| Env config | expo-constants / app.config.js extra |
react-native-config or build-time constants |
| Metro config | metro.config.js (auto-generated by Expo) |
metro.config.js (manual) |
| Native modules | Managed by Expo SDK version | Managed by your RN version + pod install |
If you are starting a new project, the Expo managed workflow (SDK 49+) is the lower-friction path: it ships Hermes as the default JS engine and includes most required crypto primitives out of the box.
| SDK Feature | Expo (SDK 49+) | Bare RN (0.71+) | Notes |
|---|---|---|---|
createWallet / importWallet |
⚠ Requires polyfill | ⚠ Requires polyfill | Needs react-native-get-random-values and Buffer polyfill |
getBalance |
⚠ Requires polyfill | ⚠ Requires polyfill | Needs fetch global; available in RN 0.72+ / Expo SDK 49+ by default |
sendXLM |
⚠ Requires polyfill | ⚠ Requires polyfill | Same as getBalance |
fundTestnetAccount |
⚠ Requires polyfill | ⚠ Requires polyfill | Testnet only; same network requirements |
getTransactions / getPayments |
⚠ Requires polyfill | ⚠ Requires polyfill | Same as getBalance |
depositToVault / withdrawFromVault / getVaultBalance |
⚠ Untested | ⚠ Untested | Pre-release; requires Soroban RPC access from mobile — see Soroban Vault Considerations |
dotenv / process.env |
❌ Not applicable | ❌ Not applicable | Use platform-native config instead — see Environment Configuration |
| Symbol | Meaning |
|---|---|
| ⚠ Requires polyfill | Works after the setup steps in this guide; not tested in the SDK's own CI |
| ❌ Not applicable | Will not work in this environment — use the documented alternative |
React Native runs on the Hermes JavaScript engine, which does not ship all
of the Node.js globals that @stellar/stellar-sdk expects. Install these
polyfills and apply them in your entry file before any SDK import.
Buffer is a Node.js global not available in Hermes.
npm install buffer// index.js — must be the very first import
import { Buffer } from 'buffer';
globalThis.Buffer = Buffer;Stellar key generation requires crypto.getRandomValues. The
react-native-get-random-values package shims it using the device's native
secure random source.
npm install react-native-get-random-values
# Bare React Native only — Expo handles this automatically
npx pod-install// index.js — import before any SDK or @stellar/stellar-sdk import
import 'react-native-get-random-values';Note
On Expo SDK 49+ with Hermes, crypto.getRandomValues is already
available via expo-crypto. You still need to import
react-native-get-random-values first, because @stellar/stellar-sdk
performs the random-values check at module load time.
React Native 0.72+ and Expo SDK 49+ include a global fetch by default. If
you are on an older version:
npm install cross-fetch// index.js
import 'cross-fetch/polyfill';Available on Hermes since React Native 0.71 and Expo SDK 48. If you are on an older version:
npm install text-encoding// index.js
import { TextEncoder, TextDecoder } from 'text-encoding';
globalThis.TextEncoder = TextEncoder;
globalThis.TextDecoder = TextDecoder;A full index.js combining all polyfills, applied in the correct order:
// index.js — top of your entry file, before any other imports
// 1. Crypto (must come first — @stellar/stellar-sdk inspects at load time)
import 'react-native-get-random-values';
// 2. Buffer
import { Buffer } from 'buffer';
globalThis.Buffer = Buffer;
// 3. fetch (remove if your RN / Expo version already includes it)
// import 'cross-fetch/polyfill';
// 4. TextEncoder / TextDecoder (remove if on RN 0.71+ / Expo SDK 48+)
// import { TextEncoder, TextDecoder } from 'text-encoding';
// globalThis.TextEncoder = TextEncoder;
// globalThis.TextDecoder = TextDecoder;
// --- Your app entry point below ---
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
AppRegistry.registerComponent(appName, () => App);Metro does not resolve Node.js built-ins by default. The @stellar/stellar-sdk
package (and its dependencies) references a handful of Node core modules that
need to be shimmed.
Install the required shims:
npm install buffer events stream-browserify readable-streamThen update (or create) metro.config.js:
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config'); // or 'metro' for bare RN
const config = getDefaultConfig(__dirname);
config.resolver.extraNodeModules = {
...config.resolver.extraNodeModules,
buffer: require.resolve('buffer'),
events: require.resolve('events'),
stream: require.resolve('stream-browserify'),
_stream_readable: require.resolve('readable-stream/readable'),
_stream_writable: require.resolve('readable-stream/writable'),
_stream_transform: require.resolve('readable-stream/transform'),
};
module.exports = config;Note
For bare React Native (without Expo), replace the first line with:
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const config = getDefaultConfig(__dirname);createWallet generates a keypair and returns it — the SDK never persists or
backs up the secret key. Your application must save secretKey to secure
storage immediately after creation. Losing it means permanent loss of access to
the wallet. See Security Best Practices.
expo-secure-store
uses the device's native keychain (iOS Keychain / Android Keystore) and is the
recommended storage layer for Expo apps.
npx expo install expo-secure-storeimport * as SecureStore from 'expo-secure-store';
import { createWallet, importWallet } from 'stellar-pocketpay-sdk';
const WALLET_KEY = 'pocketpay_secret_key';
// Saving a newly created wallet
async function createAndSaveWallet() {
const wallet = createWallet();
// Persist the secret key to the device keychain before doing anything else
await SecureStore.setItemAsync(WALLET_KEY, wallet.secretKey);
console.log('Wallet created. Public key:', wallet.publicKey);
// Never log wallet.secretKey — see Logging Guidance
return wallet.publicKey;
}
// Restoring the wallet in a later session
async function loadWallet() {
const secretKey = await SecureStore.getItemAsync(WALLET_KEY);
if (!secretKey) {
throw new Error('No wallet found in secure storage — prompt the user to create one.');
}
return importWallet(secretKey);
}Caution
expo-secure-store values are not backed up to iCloud or Google Drive
by default. If the user uninstalls the app or restores a new device, the
key is gone unless you provide a separate recovery flow (e.g. export to a
written recovery phrase). Communicate this clearly to users.
react-native-keychain
provides access to the system keychain / Keystore on bare React Native.
npm install react-native-keychain
npx pod-install # iOSimport * as Keychain from 'react-native-keychain';
import { createWallet, importWallet } from 'stellar-pocketpay-sdk';
const KEYCHAIN_SERVICE = 'pocketpay_wallet';
async function createAndSaveWallet() {
const wallet = createWallet();
await Keychain.setGenericPassword(
wallet.publicKey, // username (store the public key here)
wallet.secretKey, // password (the secret key)
{ service: KEYCHAIN_SERVICE },
);
return wallet.publicKey;
}
async function loadWallet() {
const credentials = await Keychain.getGenericPassword({ service: KEYCHAIN_SERVICE });
if (!credentials) {
throw new Error('No wallet found in keychain.');
}
return importWallet(credentials.password);
}Caution
This is a known blocker, not a recommendation. The SDK's entry point
(src/index.ts) calls dotenv.config() at module load time. dotenv depends
on the Node.js fs and path modules, which Metro cannot resolve. Importing
the SDK in React Native will fail at bundle time unless you shim these
modules. Track this in
issue #139.
Until the SDK gates this call behind a runtime check, alias dotenv to an
empty module in metro.config.js:
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');
const config = getDefaultConfig(__dirname);
config.resolver.extraNodeModules = {
...config.resolver.extraNodeModules,
dotenv: path.resolve(__dirname, 'shims/dotenv.js'),
buffer: require.resolve('buffer'),
events: require.resolve('events'),
stream: require.resolve('stream-browserify'),
};
module.exports = config;// shims/dotenv.js
module.exports = { config: () => ({ parsed: {} }) };With the shim in place, process.env reads inside the SDK resolve to
undefined and fall through to the built-in testnet defaults. The SDK will not
throw, but it will silently use Testnet. Pass configuration explicitly if you
need any other network.
dotenv reads .env files from the filesystem — it is a Node.js-only
utility and does not work in React Native. Do not call require('dotenv').config()
in a mobile app; it will either throw or silently do nothing.
Supply SDK configuration values through one of the approaches below.
Store non-secret configuration in app.config.js under the extra field:
// app.config.js
export default {
name: 'PocketPay',
slug: 'pocketpay',
extra: {
horizonUrl: process.env.HORIZON_URL ?? 'https://horizon-testnet.stellar.org',
sorobanRpcUrl: process.env.SOROBAN_RPC_URL ?? 'https://soroban-testnet.stellar.org',
vaultContractId: process.env.VAULT_CONTRACT_ID ?? '',
},
};Then read them at runtime via expo-constants:
import Constants from 'expo-constants';
const horizonUrl = Constants.expoConfig?.extra?.horizonUrl;
const sorobanRpcUrl = Constants.expoConfig?.extra?.sorobanRpcUrl;
const vaultContractId = Constants.expoConfig?.extra?.vaultContractId;Caution
Values in app.config.js extra are bundled into the app binary and
visible to anyone who inspects it. Never put secret keys, private API tokens,
or other sensitive credentials here.
react-native-config reads a
.env file at build time and injects values as native constants — it is not
dotenv at runtime, but gives a similar DX.
npm install react-native-config
npx pod-install # iOSimport Config from 'react-native-config';
const horizonUrl = Config.HORIZON_URL;
const sorobanRpcUrl = Config.SOROBAN_RPC_URL;Apply the same caution: build-time values end up in the binary.
The safest pattern is to pass configuration values directly to SDK calls rather than relying on any global injection mechanism:
import { sendXLM } from 'stellar-pocketpay-sdk';
// horizonUrl comes from your app's config layer, not from process.env
const result = await sendXLM({
sourceSecret: secretKeyFromKeychain,
destination: destinationPublicKey,
amount: '10.00',
});For vault helpers, supply sorobanRpcUrl and vaultContractId from your config
layer directly — do not depend on environment variables being available.
The vault helpers (depositToVault, withdrawFromVault, getVaultBalance)
make direct RPC calls to a deployed Soroban contract. They are pre-release
and untested in any React Native environment. Additional mobile-specific
considerations apply:
- Soroban RPC endpoint must be reachable from the device. Ensure the RPC
URL (
sorobanRpcUrl) is configured for your target network and not firewalled for mobile traffic. VAULT_CONTRACT_IDmust be supplied explicitly —process.envis not available in React Native. Read it fromexpo-constants,react-native-config, or your own config module, then pass it to the helper.- Transaction signing happens on-device using the secret key from secure storage. Never transmit the raw secret key over the network.
- These helpers are still evolving. Their call signature may change as the
underlying contract in
pocketpay-contractsmatures. Pin the SDK version and review the Changelog before upgrading.
See Soroban Vault for the full helper reference.
A minimal but complete Expo component that creates a wallet, saves it to secure storage, and fetches a balance:
import React, { useState } from 'react';
import { Button, Text, View, StyleSheet } from 'react-native';
import * as SecureStore from 'expo-secure-store';
import { createWallet, importWallet, fundTestnetAccount, getBalance } from 'stellar-pocketpay-sdk';
const WALLET_KEY = 'pocketpay_secret_key';
export default function WalletScreen() {
const [publicKey, setPublicKey] = useState<string | null>(null);
const [balance, setBalance] = useState<string | null>(null);
const [status, setStatus] = useState('');
async function handleCreateWallet() {
try {
setStatus('Creating wallet…');
const wallet = createWallet();
// Persist to device keychain immediately — SDK does not store the key
await SecureStore.setItemAsync(WALLET_KEY, wallet.secretKey);
setPublicKey(wallet.publicKey);
setStatus('Wallet created and saved to secure storage.');
} catch (err) {
setStatus(`Error: ${(err as Error).message}`);
}
}
async function handleFundAndFetchBalance() {
try {
setStatus('Loading wallet from secure storage…');
const secretKey = await SecureStore.getItemAsync(WALLET_KEY);
if (!secretKey) throw new Error('No wallet found. Create one first.');
const wallet = importWallet(secretKey);
setStatus('Funding via Friendbot (Testnet)…');
await fundTestnetAccount(wallet.publicKey);
setStatus('Fetching balance…');
const result = await getBalance(wallet.publicKey);
setBalance(result.nativeBalance);
setStatus('Done.');
} catch (err) {
setStatus(`Error: ${(err as Error).message}`);
}
}
return (
<View style={styles.container}>
<Text style={styles.heading}>PocketPay SDK — React Native Demo</Text>
<Button title="Create Wallet" onPress={handleCreateWallet} />
<Button title="Fund & Check Balance" onPress={handleFundAndFetchBalance} />
{publicKey && <Text>Public key: {publicKey}</Text>}
{balance !== null && <Text>Balance: {balance} XLM</Text>}
<Text style={styles.status}>{status}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 24, justifyContent: 'center', gap: 12 },
heading: { fontSize: 16, fontWeight: 'bold', marginBottom: 8 },
status: { color: '#666', marginTop: 8 },
});Note
Remember to set up all polyfills in index.js before this component is
loaded. See Complete polyfill entry file.
| Limitation | Detail |
|---|---|
| No official SDK test coverage for React Native | The SDK CI runs against Node.js only. Mobile behavior is best-effort guidance. |
dotenv / process.env not available |
Replace with expo-constants, react-native-config, or explicit value passing. |
| Vault helpers are pre-release | Call signature may change. Pin your SDK version and review the Changelog before upgrading. |
| Secure storage is device-local | Keys stored in expo-secure-store or react-native-keychain are not synced to cloud backup by default. Uninstalling the app destroys the key unless you provide an export/recovery flow. |
| Hermes crypto coverage varies by RN version | Older RN / Expo versions may need additional shims. Validate against your specific versions. |
| No deep imports | Only the package root (stellar-pocketpay-sdk) is the supported entry point. Do not import from internal paths. |
- Runtime Compatibility — full compatibility matrix and polyfill summary across all environments
- Security Best Practices — key management and backup responsibility
- Getting Started — general SDK setup and first-use walkthrough
- Soroban Vault — vault helper reference and limitations
- Network Error Handling — retry guidance for Horizon, Friendbot, and Soroban RPC
- Error Handling — SDK error types and handling patterns
- Logging Guidance — safe logging practices (never log secret keys)