Skip to content

Commit 4126dbe

Browse files
committed
feat(RNPSW): add resume transaction functionality and validation updates
1 parent 1d57f0d commit 4126dbe

5 files changed

Lines changed: 229 additions & 47 deletions

File tree

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,38 @@ const Checkout = () => {
102102
};
103103
```
104104

105+
### Resume transaction
106+
<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.
107+
</p>
108+
109+
```tsx
110+
import React from 'react';
111+
import { Button } from 'react-native';
112+
import { usePaystack } from 'react-native-paystack-webview';
113+
114+
const ResumeTransaction = () => {
115+
const { popup } = usePaystack();
116+
117+
const resumePayment = () => {
118+
popup.resumeTransaction({
119+
accessCode: 'ACCESS_CODE_FROM_PAYSTACK',
120+
onSuccess: (res) => console.log('Payment resumed successfully:', res),
121+
onCancel: () => console.log('User cancelled'),
122+
onLoad: (res) => console.log('WebView Loaded:', res),
123+
onError: (err) => console.log('WebView Error:', err)
124+
});
125+
};
126+
127+
return <Button title="Resume Payment" onPress={resumePayment} />;
128+
};
129+
```
130+
105131
---
106132

107133
## 🧠 Features
108134

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

176+
### `popup.resumeTransaction()`
177+
178+
Resume a transaction that was previously interrupted. This method allows users to complete a payment using an access code provided by Paystack.
179+
180+
| Param | Type | Required | Description |
181+
|---------------|---------------------|----------|-------------------------------------------|
182+
| `accessCode` | `string` || Access code from Paystack for resuming a transaction |
183+
| `onSuccess` | `(res) => void` || Called on successful payment |
184+
| `onCancel` | `() => void` || Called on cancellation |
185+
| `onLoad` | `(res) => void` || Triggered when transaction view loads |
186+
| `onError` | `(err) => void` || Triggered on WebView or script error |
187+
149188
---
150189

151190
#### Meta Props

__tests__/index.test.tsx

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { validateParams, sanitize, generatePaystackParams } from '../development/utils';
21
import { Alert } from 'react-native';
2+
import { validateParams, sanitize, generatePaystackParams } from '../development/utils';
3+
import { TransactionType } from '../development/types';
34

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

21+
it('should return true for valid resume transaction params', () => {
22+
const result = validateParams({
23+
accessCode: 'ac_123',
24+
onSuccess: jest.fn(),
25+
onCancel: jest.fn()
26+
}, false);
27+
expect(result).toBe(true);
28+
});
29+
2030
it('should fail with missing email and show alert', () => {
2131
const result = validateParams({
2232
email: '',
@@ -49,6 +59,14 @@ describe('Paystack Utils', () => {
4959
expect(result).toBe(false);
5060
expect(Alert.alert).toHaveBeenCalledWith('Payment Error', expect.stringContaining('onSuccess callback is required'));
5161
});
62+
63+
it('should fail with invalid accessCode', () => {
64+
const result = validateParams({
65+
accessCode: ' '
66+
} as any, true);
67+
expect(result).toBe(false);
68+
expect(Alert.alert).toHaveBeenCalledWith('Payment Error', expect.stringContaining('accessCode must be a non-empty string'));
69+
});
5270
});
5371

5472
describe('sanitize', () => {
@@ -79,9 +97,41 @@ describe('Paystack Utils', () => {
7997
currency: 'NGN',
8098
channels: ['card']
8199
});
82-
expect(js).toContain("key: 'pk_test'");
83-
expect(js).toContain("email: 'email@test.com'");
84-
expect(js).toContain("amount: 10000");
100+
expect(js.mode).toBe(TransactionType.STANDARD);
101+
expect(js.params).toContain("key: 'pk_test'");
102+
expect(js.params).toContain("email: 'email@test.com'");
103+
expect(js.params).toContain("amount: 10000");
104+
});
105+
106+
it('should throw error if required fields are missing', () => {
107+
expect(() => generatePaystackParams({
108+
email: 'email@test.com',
109+
amount: 100,
110+
reference: 'ref123',
111+
} as any)).toThrow('Public Key is required to generate Paystack parameters');
112+
expect(() => generatePaystackParams({
113+
publicKey: 'pk_test',
114+
amount: 100,
115+
reference: 'ref123',
116+
} as any)).toThrow('Email and Amount are required to generate Paystack parameters');
117+
expect(() => generatePaystackParams({
118+
publicKey: 'pk_test',
119+
email: 'email@test.com',
120+
reference: 'ref123',
121+
} as any)).toThrow('Email and Amount are required to generate Paystack parameters');
122+
expect(() => generatePaystackParams({
123+
publicKey: 'pk_test',
124+
email: 'email@test.com',
125+
amount: 100,
126+
} as any)).toThrow('Reference is required to generate Paystack parameters');
127+
});
128+
129+
it('should generate params for resume transaction', () => {
130+
const js = generatePaystackParams({
131+
accessCode: 'ac_123'
132+
});
133+
expect(js.mode).toBe(TransactionType.RESUME);
134+
expect(js.accessCode).toBe('ac_123');
85135
});
86136
});
87137
});

development/PaystackProvider.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
55
import {
66
PaystackParams,
77
PaystackProviderProps,
8+
TransactionMethod
89
} from './types';
910
import { validateParams, paystackHtmlContent, generatePaystackParams, handlePaystackMessage } from './utils';
1011
import { styles } from './styles';
@@ -13,6 +14,7 @@ export const PaystackContext = createContext<{
1314
popup: {
1415
checkout: (params: PaystackParams) => void;
1516
newTransaction: (params: PaystackParams) => void;
17+
resumeTransaction: (params: PaystackParams) => void;
1618
};
1719
} | null>(null);
1820

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

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

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

45-
const checkout = (params: PaystackParams) => open(params, 'checkout');
46-
const newTransaction = (params: PaystackParams) => open(params, 'newTransaction');
47+
const checkout = (params: PaystackParams) => open(params, TransactionMethod.CHECKOUT);
48+
const newTransaction = (params: PaystackParams) => open(params, TransactionMethod.NEW_TRANSACTION);
49+
const resumeTransaction = (params: PaystackParams) => open(params, TransactionMethod.RESUME_TRANSACTION);
4750

4851
const close = () => {
4952
setVisible(false);
@@ -77,6 +80,7 @@ export const PaystackProvider: React.FC<PaystackProviderProps> = ({
7780
subaccount: params.subaccount,
7881
split: params.split,
7982
split_code: params.split_code,
83+
accessCode: params.accessCode,
8084
}),
8185
method
8286
);
@@ -87,7 +91,7 @@ export const PaystackProvider: React.FC<PaystackProviderProps> = ({
8791
}
8892

8993
return (
90-
<PaystackContext.Provider value={{ popup: { checkout, newTransaction } }}>
94+
<PaystackContext.Provider value={{ popup: { checkout, newTransaction, resumeTransaction } }}>
9195
{children}
9296
<Modal visible={visible} transparent animationType="slide">
9397
<SafeAreaView style={styles.container}>

development/types.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@ export type PaystackProviderProps = {
1818
onGlobalCancel?: () => void;
1919
};
2020

21-
export type PaystackParams = {
21+
type PaystackBaseCallbacks = {
22+
onSuccess: (data: PaystackTransactionResponse) => void;
23+
onCancel: () => void;
24+
onLoad?: (res: PaystackOnloadResponse) => void;
25+
onError?: (res: any) => void;
26+
};
27+
28+
type StandardFields = {
2229
email: string;
2330
amount: number;
2431
metadata?: Record<string, any>;
@@ -28,11 +35,23 @@ export type PaystackParams = {
2835
subaccount?: string;
2936
split_code?: string;
3037
split?: DynamicMultiSplitProps;
31-
onSuccess: (data: PaystackTransactionResponse) => void;
32-
onCancel: () => void;
33-
onLoad?: (res: PaystackOnloadResponse) => void;
34-
onError?: (res: any) => void;
35-
};
38+
accessCode?: never;
39+
}
40+
41+
type ResumeTransactionFields = {
42+
accessCode: string;
43+
email?: never;
44+
amount?: never;
45+
metadata?: never;
46+
reference?: never;
47+
plan?: never;
48+
invoice_limit?: never;
49+
subaccount?: never;
50+
split_code?: never;
51+
split?: never;
52+
}
53+
54+
export type PaystackParams = PaystackBaseCallbacks & (StandardFields | ResumeTransactionFields);
3655

3756
export type PaystackCheckoutParams = {
3857
email: string;
@@ -80,3 +99,20 @@ export interface DynamicMultiSplitProps {
8099
bearer_subaccount?: string;
81100
reference?: string;
82101
}
102+
103+
export enum TransactionMethod {
104+
CHECKOUT = 'checkout',
105+
NEW_TRANSACTION = 'newTransaction',
106+
RESUME_TRANSACTION = 'resumeTransaction'
107+
}
108+
109+
export enum TransactionType {
110+
STANDARD = 'standard',
111+
RESUME = 'resume'
112+
}
113+
114+
export type PaystackGeneratedConfig = {
115+
mode: TransactionType;
116+
accessCode?: string;
117+
params: string;
118+
};

0 commit comments

Comments
 (0)