Skip to content

Latest commit

 

History

History
564 lines (430 loc) · 19.5 KB

File metadata and controls

564 lines (430 loc) · 19.5 KB

React Native Compatibility Guide

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.


Table of Contents


Expo vs Bare React Native

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.


Compatibility Summary

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

Key

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

Required Polyfills

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

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;

crypto.getRandomValues

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.

fetch

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';

TextEncoder / TextDecoder

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;

Complete polyfill entry file

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 Bundler Configuration

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-stream

Then 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);

Secure Key Storage

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: expo-secure-store

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-store
import * 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.

Bare React Native: react-native-keychain

react-native-keychain provides access to the system keychain / Keystore on bare React Native.

npm install react-native-keychain
npx pod-install   # iOS
import * 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);
}

Environment Configuration (no dotenv)

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.

Workaround: alias dotenv in Metro

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.

Expo Constants / app.config.js

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.

Bare React Native: react-native-config

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  # iOS
import 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.

Passing configuration explicitly

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.


Soroban Vault Considerations

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_ID must be supplied explicitlyprocess.env is not available in React Native. Read it from expo-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-contracts matures. Pin the SDK version and review the Changelog before upgrading.

See Soroban Vault for the full helper reference.


Integration Example

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 &amp; 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.


Known Limitations

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.

Related Docs