Skip to content

Commit 516ed3c

Browse files
authored
BREAKING CHANGE: : V5 (#210)
* BREAKING CHANGE: : V5 - βœ… Simple `checkout()` or `newTransaction()` calls - βœ… Global callbacks with `onGlobalSuccess` or `onGlobalCancel` - βœ… Debug logging with `debug` prop - βœ… Fully typed params for transactions - βœ… Works seamlessly with Expo & bare React Native - βœ… Full test coverage * BREAKING CHANGE: : V5 - nitpick fix * BREAKING CHANGE: : V5 more typing
1 parent 83390d9 commit 516ed3c

18 files changed

+602
-632
lines changed

β€Ž.DS_Store

0 Bytes
Binary file not shown.

β€ŽREADME.md

+149-113
Large diffs are not rendered by default.

β€Ž__tests__/index.test.tsx

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { validateParams, sanitize, generatePaystackParams } from '../development/utils';
2+
import { Alert } from 'react-native';
3+
4+
jest.mock('react-native', () => ({
5+
Alert: { alert: jest.fn() }
6+
}));
7+
8+
describe('Paystack Utils', () => {
9+
describe('validateParams', () => {
10+
it('should return true for valid params', () => {
11+
const result = validateParams({
12+
13+
amount: 5000,
14+
onSuccess: jest.fn(),
15+
onCancel: jest.fn()
16+
}, false);
17+
expect(result).toBe(true);
18+
});
19+
20+
it('should fail with missing email and show alert', () => {
21+
const result = validateParams({
22+
email: '',
23+
amount: 5000,
24+
onSuccess: jest.fn(),
25+
onCancel: jest.fn()
26+
}, true);
27+
expect(result).toBe(false);
28+
expect(Alert.alert).toHaveBeenCalledWith('Payment Error', expect.stringContaining('Email is required'));
29+
});
30+
31+
it('should fail with invalid amount', () => {
32+
const result = validateParams({
33+
34+
amount: 0,
35+
onSuccess: jest.fn(),
36+
onCancel: jest.fn()
37+
}, true);
38+
expect(result).toBe(false);
39+
expect(Alert.alert).toHaveBeenCalledWith('Payment Error', expect.stringContaining('Amount must be a valid number'));
40+
});
41+
42+
it('should fail with missing callbacks', () => {
43+
const result = validateParams({
44+
45+
amount: 1000,
46+
onSuccess: undefined,
47+
onCancel: undefined
48+
} as any, true);
49+
expect(result).toBe(false);
50+
expect(Alert.alert).toHaveBeenCalledWith('Payment Error', expect.stringContaining('onSuccess callback is required'));
51+
});
52+
});
53+
54+
describe('sanitize', () => {
55+
it('should wrap string by default', () => {
56+
expect(sanitize('hello', '', true)).toBe("'hello'");
57+
});
58+
59+
it('should return stringified object', () => {
60+
expect(sanitize({ test: true }, {})).toBe(JSON.stringify({ test: true }));
61+
});
62+
63+
it('should return fallback on error', () => {
64+
const circular = {};
65+
// @ts-ignore
66+
circular.self = circular;
67+
expect(sanitize(circular, 'fallback')).toBe(JSON.stringify('fallback'));
68+
});
69+
});
70+
71+
describe('generatePaystackParams', () => {
72+
it('should generate JS object string with all fields', () => {
73+
const js = generatePaystackParams({
74+
publicKey: 'pk_test',
75+
76+
amount: 100,
77+
reference: 'ref123',
78+
metadata: { order: 123 },
79+
currency: 'NGN',
80+
channels: ['card']
81+
});
82+
expect(js).toContain("key: 'pk_test'");
83+
expect(js).toContain("email: '[email protected]'");
84+
expect(js).toContain("amount: 10000");
85+
});
86+
});
87+
});

β€Ž__tests__/utils.test.ts

-95
This file was deleted.

β€Ždevelopment/PaystackProvider.tsx

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React, { createContext, useCallback, useMemo, useState } from 'react';
2+
import { Modal, View, ActivityIndicator, } from 'react-native';
3+
import { WebView, WebViewMessageEvent } from 'react-native-webview';
4+
import {
5+
PaystackParams,
6+
PaystackProviderProps,
7+
} from './types';
8+
import { validateParams, paystackHtmlContent, generatePaystackParams, handlePaystackMessage } from './utils';
9+
import { styles } from './styles';
10+
11+
export const PaystackContext = createContext<{
12+
popup: {
13+
checkout: (params: PaystackParams) => void;
14+
newTransaction: (params: PaystackParams) => void;
15+
};
16+
} | null>(null);
17+
18+
export const PaystackProvider: React.FC<PaystackProviderProps> = ({
19+
publicKey,
20+
currency = 'NGN',
21+
defaultChannels = ['card'],
22+
debug = false,
23+
children,
24+
onGlobalSuccess,
25+
onGlobalCancel,
26+
}) => {
27+
const [visible, setVisible] = useState(false);
28+
const [params, setParams] = useState<PaystackParams | null>(null);
29+
const [method, setMethod] = useState<'checkout' | 'newTransaction'>('checkout');
30+
31+
const fallbackRef = useMemo(() => `ref_${Date.now()}`, []);
32+
33+
const open = useCallback(
34+
(params: PaystackParams, selectedMethod: 'checkout' | 'newTransaction') => {
35+
if (debug) console.log(`[Paystack] Opening modal with method: ${selectedMethod}`);
36+
if (!validateParams(params, debug)) return;
37+
setParams(params);
38+
setMethod(selectedMethod);
39+
setVisible(true);
40+
},
41+
[debug]
42+
);
43+
44+
const checkout = (params: PaystackParams) => open(params, 'checkout');
45+
const newTransaction = (params: PaystackParams) => open(params, 'newTransaction');
46+
47+
const close = () => {
48+
setVisible(false);
49+
setParams(null);
50+
}
51+
52+
const handleMessage = (event: WebViewMessageEvent) => {
53+
handlePaystackMessage({
54+
event,
55+
debug,
56+
params,
57+
onGlobalSuccess,
58+
onGlobalCancel,
59+
close,
60+
});
61+
};
62+
63+
const paystackHTML = useMemo(() => {
64+
if (!params) return '';
65+
return paystackHtmlContent(
66+
generatePaystackParams({
67+
publicKey,
68+
email: params.email,
69+
amount: params.amount,
70+
reference: params.reference || fallbackRef,
71+
metadata: params.metadata,
72+
currency,
73+
channels: defaultChannels,
74+
}),
75+
method
76+
);
77+
}, [params, method]);
78+
79+
if (debug && visible) {
80+
console.log('[Paystack] HTML Injected:', paystackHTML);
81+
}
82+
83+
return (
84+
<PaystackContext.Provider value={{ popup: { checkout, newTransaction } }}>
85+
{children}
86+
<Modal visible={visible} transparent animationType="slide">
87+
<View style={styles.container}>
88+
<WebView
89+
originWhitelist={["*"]}
90+
source={{ html: paystackHTML }}
91+
onMessage={handleMessage}
92+
javaScriptEnabled
93+
domStorageEnabled
94+
startInLoadingState
95+
onLoadStart={() => debug && console.log('[Paystack] WebView Load Start')}
96+
onLoadEnd={() => debug && console.log('[Paystack] WebView Load End')}
97+
renderLoading={() => <ActivityIndicator size="large" />}
98+
/>
99+
</View>
100+
</Modal>
101+
</PaystackContext.Provider>
102+
);
103+
};

β€Ždevelopment/index.ts

-10
This file was deleted.

β€Ždevelopment/index.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { PaystackContext, PaystackProvider } from "./PaystackProvider"
2+
import { usePaystack } from "./usePaystack"
3+
import * as PaystackProps from './types'
4+
5+
export {
6+
PaystackProvider,
7+
PaystackContext,
8+
usePaystack,
9+
PaystackProps
10+
}

0 commit comments

Comments
Β (0)