Skip to content

funpm/paystack-rn-checkout-server

Repository files navigation

@funpm/paystack-rn-checkout-server

A Paystack payment SDK for React Native & Expo that solves the "what happens after payment?" problem.

Backend-initialized, WebView-powered checkout with proper lifecycle management — success detection, redirect interception, automatic verification, and clean state handling.

Requirements

This package requires a backend server. It does not support client-side (public key) initialization.

Before the checkout can open, your server must call Paystack's POST /transaction/initialize endpoint using your secret key and return the result to your app. The SDK takes it from there.

You need:

  • A backend (Node.js, Python, Go, etc.) that can call Paystack's API
  • A Paystack secret key (sk_live_... / sk_test_...) stored securely on your server — never in the app
  • The backend to expose at minimum two endpoints: initialize and verify

This package does NOT support:

  • Passing your public key directly to the SDK
  • Client-side transaction initialization
  • Paystack's Inline JS / popup embed

If you want a quick client-side integration without a backend, this is not the right package. If you want a secure, production-grade integration where your backend owns the transaction lifecycle, read on.


The Problem

Every existing Paystack React Native package has the same gaps:

  • Blank screens after payment (especially on iOS with newer Expo SDKs)
  • No clear success/failure detection — the WebView loads a redirect URL and you're left guessing
  • Public key on the frontend — initializing transactions client-side exposes your keys
  • No verification flow — payment "succeeds" in the WebView but nobody confirms it
  • Redirect hell — callback URLs load in the WebView instead of triggering app logic

This package takes a different approach: your backend owns the transaction lifecycle, and the frontend is just a secure checkout window with proper event detection.

How It Works

┌─────────────┐     ┌──────────────┐     ┌───────────┐
│  Your App    │────▶│  Your Backend │────▶│  Paystack  │
│  (RN/Expo)  │     │  (Express)    │     │  API       │
└──────┬───────┘     └──────┬────────┘     └─────┬─────┘
       │                    │                     │
       │  1. "Pay ₦5,000"  │                     │
       │───────────────────▶│                     │
       │                    │  2. POST /initialize│
       │                    │────────────────────▶│
       │                    │                     │
       │                    │  3. { access_code,  │
       │                    │     authorization_url,
       │                    │     reference }     │
       │                    │◀────────────────────│
       │  4. transaction    │                     │
       │    data            │                     │
       │◀───────────────────│                     │
       │                    │                     │
       │  5. Opens WebView  │                     │
       │     checkout ──────────────────────────▶ │
       │                    │                     │
       │  6. Customer pays  │                     │
       │     in WebView     │                     │
       │                    │                     │
       │  7. SDK detects    │                     │
       │     success via    │                     │
       │     redirect +     │                     │
       │     postMessage    │                     │
       │                    │                     │
       │  8. onSuccess()    │                     │
       │     fires          │                     │
       │                    │                     │
       │  9. Auto-verify    │                     │
       │───────────────────▶│  10. GET /verify    │
       │                    │────────────────────▶│
       │                    │                     │
       │  11. onVerified()  │  12. { status:      │
       │     fires ◀────────│      "success" }    │
       │                    │◀────────────────────│
       │                    │                     │
       │                    │  13. Webhook:        │
       │                    │  charge.success     │
       │                    │◀────────────────────│
       │                    │                     │
       │                    │  14. Deliver value  │
       │                    │     (fulfill order) │

Install

npm install @funpm/paystack-rn-checkout-server react-native-webview react-native-safe-area-context

# Expo (resolves compatible versions automatically)
npx expo install react-native-webview react-native-safe-area-context

# Bare React Native — link native modules
cd ios && pod install

Quick Start

1. Backend — Initialize & Verify

Your backend handles all Paystack API communication. Here's the minimal Express setup:

// server.js
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());

const PAYSTACK_SECRET = process.env.PAYSTACK_SECRET_KEY;

// Initialize a transaction
app.post('/payments/initialize', async (req, res) => {
  const { email, amount, channels } = req.body;

  const response = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${PAYSTACK_SECRET}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email,
      amount, // in kobo (₦100 = 10000)
      channels, // optional — restrict payment channels, e.g. ['card', 'bank_transfer']
      callback_url: 'https://paystack-rn-checkout.localhost/callback',
    }),
  });

  const data = await response.json();
  res.json(data);
});

// Verify a transaction
app.get('/payments/verify', async (req, res) => {
  const response = await fetch(
    `https://api.paystack.co/transaction/verify/${req.query.reference}`,
    { headers: { Authorization: `Bearer ${PAYSTACK_SECRET}` } }
  );
  const data = await response.json();
  res.json(data);
});

// Webhook — the REAL source of truth
app.post('/payments/webhook', (req, res) => {
  const hash = crypto
    .createHmac('sha512', PAYSTACK_SECRET)
    .update(JSON.stringify(req.body))
    .digest('hex');

  if (hash !== req.headers['x-paystack-signature']) {
    return res.sendStatus(400);
  }

  if (req.body.event === 'charge.success') {
    // Deliver value here — update DB, send emails, etc.
    console.log('Payment confirmed:', req.body.data.reference);
  }

  res.sendStatus(200);
});

2. Frontend — Open Checkout

Wrap your app with SafeAreaProvider and PaystackProvider once at the root:

// App.tsx
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { PaystackProvider } from '@funpm/paystack-rn-checkout-server';

export default function App() {
  return (
    <SafeAreaProvider>
      <PaystackProvider config={{
        verifyEndpoint: 'https://api.yourapp.com/payments/verify',
        callbackUrl: 'https://paystack-rn-checkout.localhost/callback',
      }}>
        <YourNavigator />
      </PaystackProvider>
    </SafeAreaProvider>
  );
}

Then in your payment screen, use the usePaystackCheckout hook:

import {
  PaystackCheckout,
  usePaystackCheckout,
  type InitializeTransactionBody,
} from '@funpm/paystack-rn-checkout-server';

export default function PaymentScreen() {
  const checkout = usePaystackCheckout({
    verifyEndpoint: 'https://api.yourapp.com/payments/verify',
    onSuccess: (data) => {
      // Checkout UI completed. Don't deliver value yet — wait for onVerified.
      console.log('Reference:', data.reference);
    },
    onVerified: (data) => {
      if (data.success) {
        // Payment confirmed by your backend — safe to deliver value.
        // Navigate, show a success screen, update UI, etc.
      }
    },
    onCancel: () => {
      // User closed without paying — or autoClose fired after success.
    },
    onError: (error) => {
      console.error(error.code, error.message);
    },
  });

  const handlePay = async () => {
    const body: InitializeTransactionBody = {
      email: 'user@example.com',
      amount: 500000, // ₦5,000 in kobo
    };

    const res = await fetch('https://api.yourapp.com/payments/initialize', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });
    const { data } = await res.json();
    checkout.open(data);
  };

  return (
    <View style={{ flex: 1, justifyContent: 'center', padding: 24 }}>
      <Button title="Pay ₦5,000" onPress={handlePay} />

      {checkout.currentTransaction && (
        <PaystackCheckout
          visible={checkout.isVisible}
          transaction={checkout.currentTransaction}
          onSuccess={checkout.handleSuccess}
          onCancel={checkout.handleCancel}
          onError={checkout.handleError}
        />
      )}
    </View>
  );
}

Post-Payment UI

The SDK fires callbacks and exposes a state value — what you render after payment is entirely your own UI. There is no post-payment overlay built into the SDK.

Option A — Replace the screen on success

const [verifiedData, setVerifiedData] = useState(null);

const checkout = usePaystackCheckout({
  onVerified: (data) => setVerifiedData(data),
});

if (verifiedData?.success) {
  return <SuccessScreen reference={verifiedData.reference} />;
}
if (verifiedData && !verifiedData.success) {
  return <FailureScreen status={verifiedData.status} />;
}

Option B — Show a modal / bottom sheet

const [result, setResult] = useState(null);

const checkout = usePaystackCheckout({
  onVerified: (data) => setResult(data),
});

// Then render your own <Modal visible={!!result} ...>

Option C — Navigate with React Navigation or Expo Router

const checkout = usePaystackCheckout({
  onVerified: (data) => {
    if (data.success) {
      navigation.replace('PaymentSuccess', { reference: data.reference });
    } else {
      navigation.replace('PaymentFailed');
    }
  },
  onCancel: () => {
    navigation.goBack();
  },
});

Payment Channels

Channels (card, bank transfer, USSD, etc.) are set server-side during transaction initialization — they cannot be changed from the frontend after a transaction is created.

Pass them in your initialization request body:

import type { InitializeTransactionBody } from '@funpm/paystack-rn-checkout-server';

const body: InitializeTransactionBody = {
  email: 'user@example.com',
  amount: 500000,
  channels: ['card', 'bank_transfer'], // only show these two in checkout
};

Omit channels to allow all channels enabled on your Paystack account.

API Reference

<PaystackCheckout />

The main checkout component. Renders a full-screen modal with a WebView.

Prop Type Required Description
visible boolean Yes Controls modal visibility
transaction InitializedTransaction Yes The { access_code, authorization_url, reference } from your backend
onSuccess (data: CheckoutSuccessData) => void Yes Fires when payment completes in the WebView
onCancel (data: CheckoutCancelData) => void Yes Fires when the checkout closes without completed payment, or when autoClose dismisses after success (data.reason === 'auto_closed')
onError (error: CheckoutError) => void No Fires on WebView or verification errors
verifyEndpoint string No Your backend's verify URL — enables auto-verification
onVerified (data: VerifiedTransactionData) => void No Fires after verification completes
verifyDelay number No Ms to wait before first verify attempt (default: 2000)
verifyRetries number No Max verification attempts on failure (default: 3)
callbackUrl string No URL the SDK intercepts as the "done" signal (default: https://paystack-rn-checkout.localhost/callback)
autoClose boolean No Auto-dismiss modal after success. Fires onCancel with reason: 'auto_closed' (default: false)
autoCloseDelay number No Ms before auto-close fires (default: 1500)
showCloseButton boolean No Show the default ✕ button (default: true)
renderCloseButton (onClose: () => void) => ReactNode No Replace the default close button
renderLoading () => ReactNode No Replace the default loading state
renderError (error: CheckoutError, onRetry: () => void) => ReactNode No Replace the default error state

usePaystackCheckout(options)

Hook for imperative control over the checkout flow.

const checkout = usePaystackCheckout({
  onSuccess: (data) => { /* checkout completed */ },
  onVerified: (data) => { /* verification result */ },
  onCancel: () => { /* user cancelled */ },
  onError: (error) => { /* error occurred */ },
  verifyEndpoint: 'https://api.yourapp.com/payments/verify',
  autoVerify: true, // default true when verifyEndpoint is set
});

// Open checkout with a transaction from your backend
checkout.open(transaction);

// Close programmatically (fires onCancel if not already completed)
checkout.close();

// State machine
checkout.state;           // 'idle' | 'checkout' | 'verifying' | 'success' | 'failed' | 'cancelled' | 'error'
checkout.isVisible;       // boolean
checkout.reference;       // string | null
checkout.error;           // CheckoutError | null
checkout.verifiedData;    // VerifiedTransactionData | null
checkout.currentTransaction; // InitializedTransaction | null

// Wire these directly to <PaystackCheckout />
checkout.handleSuccess;   // (data: CheckoutSuccessData) => void
checkout.handleCancel;    // () => void
checkout.handleError;     // (error: CheckoutError) => void

<PaystackProvider />

Context provider for default configuration. Wrap your app (or just the payment section) once.

<PaystackProvider config={{
  verifyEndpoint: 'https://api.yourapp.com/payments/verify',
  callbackUrl: 'https://paystack-rn-checkout.localhost/callback',
  verifyDelay: 2000,
  verifyRetries: 3,
}}>
  <App />
</PaystackProvider>

How Success Detection Works

The SDK uses three layers to detect payment completion, so it never misses:

  1. URL Interception — Monitors WebView navigation for the callback URL redirect. When Paystack redirects to your callback_url with ?trxref=xxx, the SDK intercepts it before the page loads.

  2. Injected JavaScript — A script injected into the WebView listens for Paystack's postMessage events (success, close, payment.success). This catches completions that don't trigger a full redirect.

  3. DOM Observation — A MutationObserver watches for Paystack's cancellation UI text, catching edge cases where neither redirect nor postMessage fires.

Verification Strategy

Never trust the frontend alone. The onSuccess callback means the customer completed the checkout flow — it does NOT mean money landed in your account.

Always verify, and prefer webhooks:

Method When to Use Reliability
Webhooks (recommended) Always set up Paystack pushes to you — most reliable
Auto-verify via verifyEndpoint For instant UI feedback SDK calls your backend after success
Manual verify Fallback / reconciliation Call your backend's verify endpoint yourself

The recommended pattern: use onSuccess to show a "confirming..." state, use onVerified to update your UI, and use webhooks on your backend to actually deliver value.

Important: callback_url

The callback_url is how Paystack tells your app "payment is done." In a web app, this redirects the browser. In a mobile app, the SDK intercepts this URL before it loads.

Your backend's callback_url and the SDK's callbackUrl prop must match.

The default https://paystack-rn-checkout.localhost/callback is intentionally unresolvable — the WebView catches it before any network request goes out.

Customization

Custom Loading Screen

<PaystackCheckout
  renderLoading={() => (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <YourSpinner />
      <Text>Preparing secure checkout...</Text>
    </View>
  )}
/>

Custom Error Screen

<PaystackCheckout
  renderError={(error, onRetry) => (
    <View>
      <Text>{error.message}</Text>
      <Button title="Retry" onPress={onRetry} />
    </View>
  )}
/>

Custom Close Button

<PaystackCheckout
  renderCloseButton={(onClose) => (
    <TouchableOpacity onPress={onClose}>
      <Icon name="arrow-left" />
    </TouchableOpacity>
  )}
/>

TypeScript

Fully typed. All types are exported:

import type {
  // Transaction
  InitializedTransaction,
  InitializeTransactionBody,
  // Callbacks
  CheckoutSuccessData,
  CheckoutCancelData,
  CheckoutError,
  VerifiedTransactionData,
  // Hook
  UsePaystackCheckoutOptions,
  UsePaystackCheckoutReturn,
  CheckoutState,
  // Config
  PaystackCheckoutConfig,
  PaystackProviderConfig,
  // Primitives
  PaymentChannel,
  PaystackCurrency,
  TransactionStatus,
} from '@funpm/paystack-rn-checkout-server';

License

MIT

About

A robust Paystack payment SDK for React Native & Expo, backend-initialized, WebView-powered checkout with success detection, verification, and clean lifecycle management.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors