npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@funpm/paystack-rn-checkout-server

v1.0.2

Published

A robust Paystack payment SDK for React Native & Expo. Backend-initialized, WebView-powered checkout with proper lifecycle management — success handling, redirect control, polling, and deep link support.

Readme

@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: '[email protected]',
      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: '[email protected]',
  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 | | webViewProps | Partial<WebViewProps> | No | Override the underlying <WebView /> props (e.g. domStorageEnabled, thirdPartyCookiesEnabled). Core props wired internally for success/cancel detection cannot be overridden this way. Shipped defaults are what Paystack's checkout page needs to render — only set this if you've confirmed you need something different. |

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.

Manual QA Checklist

The WebView firing onLoadEnd only means the network request for the checkout page succeeded — it does not mean Paystack's checkout UI actually rendered. A page can load successfully and still throw during its own JS bootstrap (e.g. if a browser API it depends on is unavailable), leaving a blank screen with no onError/onHttpError signal, since that's a runtime error inside the page, not a network failure.

Before shipping a release that touches PaystackCheckout or its WebView configuration, manually verify on a real device (simulators can behave differently for storage/cookies):

  1. Open the checkout for a test transaction and confirm you see the actual payment method fields (card input, bank list, etc.) — not just that the loading spinner disappears.
  2. Complete a test card payment that requires 3-D Secure / OTP end-to-end and confirm the OTP redirect renders and the flow reaches onSuccess. This step depends on cookies and cross-origin resources, so it's the one most likely to regress if thirdPartyCookiesEnabled / sharedCookiesEnabled are ever turned off.
  3. Repeat on both iOS and Android — WebView storage/cookie behavior differs per platform.

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