Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,38 @@ const Checkout = () => {
};
```

### Resume transaction
<p> The resume transaction flow allows you to initiate a transaction on your server and complete it in the app. This flow provides both the security of server initialization and the convenience of the user experience in the app.
</p>

```tsx
import React from 'react';
import { Button } from 'react-native';
import { usePaystack } from 'react-native-paystack-webview';

const ResumeTransaction = () => {
const { popup } = usePaystack();

const resumePayment = () => {
popup.resumeTransaction({
accessCode: 'ACCESS_CODE_FROM_PAYSTACK',
onSuccess: (res) => console.log('Payment resumed successfully:', res),
onCancel: () => console.log('User cancelled'),
onLoad: (res) => console.log('WebView Loaded:', res),
onError: (err) => console.log('WebView Error:', err)
});
};

return <Button title="Resume Payment" onPress={resumePayment} />;
};
```

---

## 🧠 Features

- ✅ Simple `checkout()` or `newTransaction()` calls
- ✅ Simple `checkout()`, `newTransaction()`, or `resumeTransaction()` calls
- ✅ Resume interrupted transactions with access codes
- ✅ Global callbacks with `onGlobalSuccess` or `onGlobalCancel`
- ✅ Debug logging with `debug` prop
- ✅ Fully typed params for transactions
Expand Down Expand Up @@ -146,6 +173,18 @@ const Checkout = () => {
| `onLoad` | `(res) => void` | — | Triggered when transaction view loads |
| `onError` | `(err) => void` | — | Triggered on WebView or script error |

### `popup.resumeTransaction()`

Resume a transaction that was previously interrupted. This method allows users to complete a payment using an access code provided by Paystack.

| Param | Type | Required | Description |
|---------------|---------------------|----------|-------------------------------------------|
| `accessCode` | `string` | ✅ | Access code from Paystack for resuming a transaction |
| `onSuccess` | `(res) => void` | ✅ | Called on successful payment |
| `onCancel` | `() => void` | ✅ | Called on cancellation |
| `onLoad` | `(res) => void` | — | Triggered when transaction view loads |
| `onError` | `(err) => void` | — | Triggered on WebView or script error |

---

#### Meta Props
Expand Down
58 changes: 54 additions & 4 deletions __tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { validateParams, sanitize, generatePaystackParams } from '../development/utils';
import { Alert } from 'react-native';
import { validateParams, sanitize, generatePaystackParams } from '../development/utils';
import { TransactionType } from '../development/types';

jest.mock('react-native', () => ({
Alert: { alert: jest.fn() }
Expand All @@ -17,6 +18,15 @@ describe('Paystack Utils', () => {
expect(result).toBe(true);
});

it('should return true for valid resume transaction params', () => {
const result = validateParams({
accessCode: 'ac_123',
onSuccess: jest.fn(),
onCancel: jest.fn()
}, false);
expect(result).toBe(true);
});

it('should fail with missing email and show alert', () => {
const result = validateParams({
email: '',
Expand Down Expand Up @@ -49,6 +59,14 @@ describe('Paystack Utils', () => {
expect(result).toBe(false);
expect(Alert.alert).toHaveBeenCalledWith('Payment Error', expect.stringContaining('onSuccess callback is required'));
});

it('should fail with invalid accessCode', () => {
const result = validateParams({
accessCode: ' '
} as any, true);
expect(result).toBe(false);
expect(Alert.alert).toHaveBeenCalledWith('Payment Error', expect.stringContaining('accessCode must be a non-empty string'));
});
});

describe('sanitize', () => {
Expand Down Expand Up @@ -79,9 +97,41 @@ describe('Paystack Utils', () => {
currency: 'NGN',
channels: ['card']
});
expect(js).toContain("key: 'pk_test'");
expect(js).toContain("email: 'email@test.com'");
expect(js).toContain("amount: 10000");
expect(js.mode).toBe(TransactionType.STANDARD);
expect(js.params).toContain("key: 'pk_test'");
expect(js.params).toContain("email: 'email@test.com'");
expect(js.params).toContain("amount: 10000");
});

it('should throw error if required fields are missing', () => {
expect(() => generatePaystackParams({
email: 'email@test.com',
amount: 100,
reference: 'ref123',
} as any)).toThrow('Public Key is required to generate Paystack parameters');
expect(() => generatePaystackParams({
publicKey: 'pk_test',
amount: 100,
reference: 'ref123',
} as any)).toThrow('Email and Amount are required to generate Paystack parameters');
expect(() => generatePaystackParams({
publicKey: 'pk_test',
email: 'email@test.com',
reference: 'ref123',
} as any)).toThrow('Email and Amount are required to generate Paystack parameters');
expect(() => generatePaystackParams({
publicKey: 'pk_test',
email: 'email@test.com',
amount: 100,
} as any)).toThrow('Reference is required to generate Paystack parameters');
});

it('should generate params for resume transaction', () => {
const js = generatePaystackParams({
accessCode: 'ac_123'
});
expect(js.mode).toBe(TransactionType.RESUME);
expect(js.accessCode).toBe('ac_123');
});
});
});
14 changes: 9 additions & 5 deletions development/PaystackProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import {
PaystackParams,
PaystackProviderProps,
TransactionMethod
} from './types';
import { validateParams, paystackHtmlContent, generatePaystackParams, handlePaystackMessage } from './utils';
import { styles } from './styles';
Expand All @@ -13,6 +14,7 @@ export const PaystackContext = createContext<{
popup: {
checkout: (params: PaystackParams) => void;
newTransaction: (params: PaystackParams) => void;
resumeTransaction: (params: PaystackParams) => void;
};
} | null>(null);

Expand All @@ -27,12 +29,12 @@ export const PaystackProvider: React.FC<PaystackProviderProps> = ({
}) => {
const [visible, setVisible] = useState(false);
const [params, setParams] = useState<PaystackParams | null>(null);
const [method, setMethod] = useState<'checkout' | 'newTransaction'>('checkout');
const [method, setMethod] = useState<TransactionMethod>(TransactionMethod.CHECKOUT);

const fallbackRef = useMemo(() => `ref_${Date.now()}`, []);

const open = useCallback(
(params: PaystackParams, selectedMethod: 'checkout' | 'newTransaction') => {
(params: PaystackParams, selectedMethod: TransactionMethod) => {
if (debug) console.log(`[Paystack] Opening modal with method: ${selectedMethod}`);
if (!validateParams(params, debug)) return;
setParams(params);
Expand All @@ -42,8 +44,9 @@ export const PaystackProvider: React.FC<PaystackProviderProps> = ({
[debug]
);

const checkout = (params: PaystackParams) => open(params, 'checkout');
const newTransaction = (params: PaystackParams) => open(params, 'newTransaction');
const checkout = (params: PaystackParams) => open(params, TransactionMethod.CHECKOUT);
const newTransaction = (params: PaystackParams) => open(params, TransactionMethod.NEW_TRANSACTION);
const resumeTransaction = (params: PaystackParams) => open(params, TransactionMethod.RESUME_TRANSACTION);

const close = () => {
setVisible(false);
Expand Down Expand Up @@ -77,6 +80,7 @@ export const PaystackProvider: React.FC<PaystackProviderProps> = ({
subaccount: params.subaccount,
split: params.split,
split_code: params.split_code,
accessCode: params.accessCode,
}),
method
);
Expand All @@ -87,7 +91,7 @@ export const PaystackProvider: React.FC<PaystackProviderProps> = ({
}

return (
<PaystackContext.Provider value={{ popup: { checkout, newTransaction } }}>
<PaystackContext.Provider value={{ popup: { checkout, newTransaction, resumeTransaction } }}>
{children}
<Modal visible={visible} transparent animationType="slide">
<SafeAreaView style={styles.container}>
Expand Down
48 changes: 42 additions & 6 deletions development/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ export type PaystackProviderProps = {
onGlobalCancel?: () => void;
};

export type PaystackParams = {
type PaystackBaseCallbacks = {
onSuccess: (data: PaystackTransactionResponse) => void;
onCancel: () => void;
onLoad?: (res: PaystackOnloadResponse) => void;
onError?: (res: any) => void;
};

type StandardFields = {
email: string;
amount: number;
metadata?: Record<string, any>;
Expand All @@ -28,11 +35,23 @@ export type PaystackParams = {
subaccount?: string;
split_code?: string;
split?: DynamicMultiSplitProps;
onSuccess: (data: PaystackTransactionResponse) => void;
onCancel: () => void;
onLoad?: (res: PaystackOnloadResponse) => void;
onError?: (res: any) => void;
};
accessCode?: never;
}

type ResumeTransactionFields = {
accessCode: string;
email?: never;
amount?: never;
metadata?: never;
reference?: never;
plan?: never;
invoice_limit?: never;
subaccount?: never;
split_code?: never;
split?: never;
}

export type PaystackParams = PaystackBaseCallbacks & (StandardFields | ResumeTransactionFields);

export type PaystackCheckoutParams = {
email: string;
Expand Down Expand Up @@ -80,3 +99,20 @@ export interface DynamicMultiSplitProps {
bearer_subaccount?: string;
reference?: string;
}

export enum TransactionMethod {
CHECKOUT = 'checkout',
NEW_TRANSACTION = 'newTransaction',
RESUME_TRANSACTION = 'resumeTransaction'
}

export enum TransactionType {
STANDARD = 'standard',
RESUME = 'resume'
}

export type PaystackGeneratedConfig = {
mode: TransactionType;
accessCode?: string;
params: string;
};
Loading