Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Binary file modified .DS_Store
Binary file not shown.
262 changes: 149 additions & 113 deletions README.md

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions __tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { validateParams, sanitize, generatePaystackParams } from '../development/utils';
import { Alert } from 'react-native';

jest.mock('react-native', () => ({
Alert: { alert: jest.fn() }
}));

describe('Paystack Utils', () => {
describe('validateParams', () => {
it('should return true for valid params', () => {
const result = validateParams({
email: 'test@example.com',
amount: 5000,
onSuccess: jest.fn(),
onCancel: jest.fn()
}, false);
expect(result).toBe(true);
});

it('should fail with missing email and show alert', () => {
const result = validateParams({
email: '',
amount: 5000,
onSuccess: jest.fn(),
onCancel: jest.fn()
}, true);
expect(result).toBe(false);
expect(Alert.alert).toHaveBeenCalledWith('Payment Error', expect.stringContaining('Email is required'));
});

it('should fail with invalid amount', () => {
const result = validateParams({
email: 'test@example.com',
amount: 0,
onSuccess: jest.fn(),
onCancel: jest.fn()
}, true);
expect(result).toBe(false);
expect(Alert.alert).toHaveBeenCalledWith('Payment Error', expect.stringContaining('Amount must be a valid number'));
});

it('should fail with missing callbacks', () => {
const result = validateParams({
email: 'test@example.com',
amount: 1000,
onSuccess: undefined,
onCancel: undefined
} as any, true);
expect(result).toBe(false);
expect(Alert.alert).toHaveBeenCalledWith('Payment Error', expect.stringContaining('onSuccess callback is required'));
});
});

describe('sanitize', () => {
it('should wrap string by default', () => {
expect(sanitize('hello', '', true)).toBe("'hello'");
});

it('should return stringified object', () => {
expect(sanitize({ test: true }, {})).toBe(JSON.stringify({ test: true }));
});

it('should return fallback on error', () => {
const circular = {};
// @ts-ignore
circular.self = circular;
expect(sanitize(circular, 'fallback')).toBe(JSON.stringify('fallback'));
});
});

describe('generatePaystackParams', () => {
it('should generate JS object string with all fields', () => {
const js = generatePaystackParams({
publicKey: 'pk_test',
email: 'email@test.com',
amount: 100,
reference: 'ref123',
metadata: { order: 123 },
currency: 'NGN',
channels: ['card']
});
expect(js).toContain("key: 'pk_test'");
expect(js).toContain("email: 'email@test.com'");
expect(js).toContain("amount: 10000");
});
});
});
95 changes: 0 additions & 95 deletions __tests__/utils.test.ts

This file was deleted.

103 changes: 103 additions & 0 deletions development/PaystackProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, { createContext, useCallback, useMemo, useState } from 'react';
import { Modal, View, ActivityIndicator, } from 'react-native';
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import {
PaystackParams,
PaystackProviderProps,
} from './types';
import { validateParams, paystackHtmlContent, generatePaystackParams, handlePaystackMessage } from './utils';
import { styles } from './styles';

export const PaystackContext = createContext<{
popup: {
checkout: (params: PaystackParams) => void;
newTransaction: (params: PaystackParams) => void;
};
} | null>(null);

export const PaystackProvider: React.FC<PaystackProviderProps> = ({
publicKey,
currency = 'NGN',
defaultChannels = ['card'],
debug = false,
children,
onGlobalSuccess,
onGlobalCancel,
}) => {
const [visible, setVisible] = useState(false);
const [params, setParams] = useState<PaystackParams | null>(null);
const [method, setMethod] = useState<'checkout' | 'newTransaction'>('checkout');

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

const open = useCallback(
(params: PaystackParams, selectedMethod: 'checkout' | 'newTransaction') => {
if (debug) console.log(`[Paystack] Opening modal with method: ${selectedMethod}`);
if (!validateParams(params, debug)) return;
setParams(params);
setMethod(selectedMethod);
setVisible(true);
},
[debug]
);

const checkout = (params: PaystackParams) => open(params, 'checkout');
const newTransaction = (params: PaystackParams) => open(params, 'newTransaction');

const close = () => {
setVisible(false);
setParams(null);
}

const handleMessage = (event: WebViewMessageEvent) => {
handlePaystackMessage({
event,
debug,
params,
onGlobalSuccess,
onGlobalCancel,
close,
});
};

const paystackHTML = useMemo(() => {
if (!params) return '';
return paystackHtmlContent(
generatePaystackParams({
publicKey,
email: params.email,
amount: params.amount,
reference: params.reference || fallbackRef,
metadata: params.metadata,
currency,
channels: defaultChannels,
}),
method
);
}, [params, method]);

if (debug && visible) {
console.log('[Paystack] HTML Injected:', paystackHTML);
}

return (
<PaystackContext.Provider value={{ popup: { checkout, newTransaction } }}>
{children}
<Modal visible={visible} transparent animationType="slide">
<View style={styles.container}>
<WebView
originWhitelist={["*"]}
source={{ html: paystackHTML }}
onMessage={handleMessage}
javaScriptEnabled
domStorageEnabled
startInLoadingState
onLoadStart={() => debug && console.log('[Paystack] WebView Load Start')}
onLoadEnd={() => debug && console.log('[Paystack] WebView Load End')}
renderLoading={() => <ActivityIndicator size="large" />}
/>
</View>
</Modal>
</PaystackContext.Provider>
);
};
10 changes: 0 additions & 10 deletions development/index.ts

This file was deleted.

10 changes: 10 additions & 0 deletions development/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { PaystackContext, PaystackProvider } from "./PaystackProvider"
import { usePaystack } from "./usePaystack"
import * as PaystackProps from './types'

export {
PaystackProvider,
PaystackContext,
usePaystack,
PaystackProps
}
Loading