├── .husky ├── .gitignore └── pre-commit ├── _config.yml ├── .DS_Store ├── babel.config.js ├── .prettierrc ├── release.config.cjs ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ └── bug_report.md ├── pull_request_template.md └── workflows │ ├── pr-test.yml │ └── release.yml ├── development ├── styles.ts ├── index.tsx ├── usePaystack.tsx ├── types.ts ├── PaystackProvider.tsx └── utils.ts ├── tsconfig.json ├── .gitignore ├── LICENSE ├── CONTRIBUTING.md ├── .eslintrc.js ├── package.json ├── __tests__ └── index.test.tsx ├── .all-contributorsrc └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/just1and0/React-Native-Paystack-WebView/HEAD/.DS_Store -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /release.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches:[ "main", { name: "next", prerelease: true}], 3 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [just1and0] 4 | patreon: just1and0 5 | ko_fi: just1and0 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /development/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | backgroundColor: 'white' 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /development/index.tsx: -------------------------------------------------------------------------------- 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 | } -------------------------------------------------------------------------------- /development/usePaystack.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { PaystackContext } from './PaystackProvider'; 3 | 4 | export const usePaystack = () => { 5 | const context = useContext(PaystackContext); 6 | if (!context) throw new Error('usePaystack must be used within a PaystackProvider'); 7 | return context; 8 | }; -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | ## Issue URL 5 | 6 | 7 | ## Before and After 8 | Add Image/video/gifs of changes 9 | 10 | | Before | After | 11 | | --- | --- | 12 | | **Visual:** | **Visual:** | 13 | | **Functionality:** | **Functionality:** | 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "jsx": "react-native", 6 | "lib": ["ESNext"], 7 | "module": "CommonJS", 8 | "noEmitOnError": true, 9 | "outDir": "./production/lib", 10 | "skipLibCheck": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "target": "ESNext", 14 | "typeRoots": ["./lib/types"] 15 | }, 16 | "exclude": ["**/__tests__/*"], 17 | "include": ["development", "__tests__/index.test.tsx"] 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/pr-test.yml: -------------------------------------------------------------------------------- 1 | name: Run Build and Tests on Pull Request 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - reopened 8 | - synchronize 9 | 10 | jobs: 11 | test-build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: "lts/*" 18 | - run: npm install --legacy-peer-deps 19 | - name: Run Tests 20 | run: | 21 | npx jest --ci --coverage --maxWorkers=2 22 | continue-on-error: false 23 | - name: TypeScript Check 24 | run: yarn tsc 25 | - run: yarn build 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when I setup the package in my React Native JS project, it seem the project has an issue with imported types in javascript. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: NEW RELEASE 🥳 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | 9 | permissions: 10 | contents: write 11 | issues: write 12 | pull-requests: write 13 | 14 | jobs: 15 | release: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: "lts/*" 22 | - run: npm install --legacy-peer-deps 23 | - name: Run Tests 24 | run: | 25 | npx jest --ci --coverage --maxWorkers=2 26 | continue-on-error: false 27 | - name: TypeScript Check 28 | run: yarn tsc 29 | - run: yarn build 30 | - name: Release 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | run: npx semantic-release 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IJ 26 | # 27 | *.iml 28 | .idea 29 | .gradle 30 | local.properties 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | 37 | production/ 38 | # yarn 39 | yarn-error.log 40 | 41 | # BUCK 42 | buck-out/ 43 | \.buckd/ 44 | android/app/libs 45 | android/keystores/debug.keystore 46 | package-json.lock 47 | 48 | # vscode 49 | .vscode 50 | examples/mlkit/android/app/google-services.json 51 | examples/mlkit/ios/Pods 52 | examples/mlkit/ios/mlkit/GoogleService-Info.plist 53 | 54 | !debug.keystore 55 | /ios/Pods/ 56 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Oluwatobi Shokunbi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | We’re excited to have you contribute! Here’s how to get started: 4 | 5 | ## Steps to Contribute 6 | 7 | - Fork the Project: Start by forking this repository to your own GitHub account. 8 | - Create a New Branch: For each new feature or change, create a new branch from the `main` branch. 9 | - Make Your Changes. 10 | - Submit a Pull Request (PR): Once you’re satisfied with your changes, submit a PR. Be sure to provide a clear description of what your PR does and the problem it solves. 11 | - Add @just1and0 as a reviewer. 12 | 13 | ## Important Notes 14 | - All commit titles must begin with either; 15 | - fix(RNPSW): : this is a fix PR 16 | - feat(RNPSW): : this is a new feature PR 17 | - perf(RNPSW): : this is a performance PR 18 | - BREAKING CHANGE: : this is a breaking change 19 | - No Build Attempts in PRs: Please do not try to build the project within your PR; our Continuous Integration (CI) system will handle that for you. 20 | - Contributor Recognition: You do not need to add yourself as a contributor; the CI will automatically recognize and credit you once your PR is merged. 21 | 22 | Thank you for helping us make this project even better! We appreciate your contributions! 23 | 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const IGNORE = 0; 2 | const WARN = 1; 3 | const ERROR = 2; 4 | 5 | module.exports = { 6 | env: { 7 | browser: true, 8 | node: true, 9 | jest: true, 10 | es6: true, 11 | }, 12 | extends: [ 13 | 'plugin:react/recommended', 14 | 'airbnb', 15 | 'plugin:import/warnings', 16 | 'plugin:import/typescript', 17 | 'plugin:import/errors', 18 | ], 19 | parser: '@typescript-eslint/parser', 20 | parserOptions: { 21 | ecmaVersion: 6, 22 | sourceType: 'module', 23 | ecmaFeatures: { 24 | modules: true, 25 | }, 26 | }, 27 | plugins: ['react', 'react-native', '@typescript-eslint', 'import'], 28 | rules: { 29 | // Allow imports from dev and peer dependencies 30 | 'import/no-extraneous-dependencies': [ERROR, { devDependencies: true, peerDependencies: true }], 31 | 'no-unused-vars': WARN, 32 | 'no-shadow': IGNORE, 33 | 'import/no-unresolved': IGNORE, 34 | 'import/extensions': [ 35 | ERROR, 36 | 'ignorePackages', 37 | { 38 | js: 'never', 39 | jsx: 'never', 40 | ts: 'never', 41 | tsx: 'never', 42 | }, 43 | ], 44 | 'react/prop-types': IGNORE, 45 | 'no-use-before-define': IGNORE, 46 | 'react/jsx-filename-extension': IGNORE, 47 | 'no-case-declarations': IGNORE, 48 | 'no-unused-expressions': IGNORE, 49 | 'object-curly-newline': IGNORE, 50 | 'prefer-template': IGNORE, 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-paystack-webview", 3 | "version": "4.1.1-development", 4 | "description": "This package allows you to accept payment in your react native project using paystack ", 5 | "homepage": "https://github.com/just1and0/React-Native-Paystack-WebView#readme", 6 | "main": "production/lib/index.js", 7 | "keywords": [ 8 | "react-native", 9 | "paystack", 10 | "paystack-webview", 11 | "react-native-payments", 12 | "payments", 13 | "subscription", 14 | "split-payments", 15 | "multi-split", 16 | "webview", 17 | "react-native-webview", 18 | "checkout", 19 | "mobile-payments", 20 | "hook", 21 | "provider", 22 | "africa", 23 | "nigeria", 24 | "fintech" 25 | ], 26 | "types": "production/lib/index.d.ts", 27 | "author": "Oluwatobi Shokunbi ", 28 | "license": "MIT", 29 | "scripts": { 30 | "build": "rm -rf production && tsc -p .", 31 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 32 | "test": "jest", 33 | "semantic-release": "semantic-release" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.22.0", 37 | "@babel/runtime": "^7.14.6", 38 | "@react-native-community/eslint-config": "^3.0.0", 39 | "@testing-library/jest-native": "^5.4.3", 40 | "@testing-library/react-native": "^13.0.0", 41 | "@types/jest": "^29.5.14", 42 | "@types/react-native": "^0.64.12", 43 | "@types/validator": "^13.6.3", 44 | "babel-jest": "^26.6.3", 45 | "eslint": "^7.30.0", 46 | "eslint-plugin-simple-import-sort": "^7.0.0", 47 | "jest": "^29.7.0", 48 | "jest-react-native": "^18.0.0", 49 | "metro-react-native-babel-preset": "^0.66.1", 50 | "react": "^17.0.1", 51 | "react-native": "^0.64.2", 52 | "react-test-renderer": "^19.0.0", 53 | "semantic-release": "^24.0.0", 54 | "typescript": "^4.3.5" 55 | }, 56 | "peerDependencies": { 57 | "react": "*", 58 | "react-native": "*", 59 | "react-native-webview": "*" 60 | }, 61 | "dependencies": { 62 | "@types/validator": "^13.6.3", 63 | "react-native-webview": "*", 64 | "validator": "^13.6.0" 65 | }, 66 | "files": [ 67 | "production", 68 | "README.md", 69 | "LICENSE", 70 | "package.json", 71 | "contributing.md" 72 | ] 73 | } -------------------------------------------------------------------------------- /development/types.ts: -------------------------------------------------------------------------------- 1 | export type PaystackTransactionResponse = { 2 | reference: string; 3 | trans: string; 4 | transaction: string; 5 | status: string; 6 | message?: string; 7 | }; 8 | 9 | export type PaystackOnloadResponse = { id: string; accessCode: string; customer: Record } 10 | 11 | export type PaystackProviderProps = { 12 | publicKey: string; 13 | currency?: Currency; 14 | defaultChannels?: PaymentChannels; 15 | debug?: boolean; 16 | children: React.ReactNode; 17 | onGlobalSuccess?: (data: PaystackTransactionResponse) => void; 18 | onGlobalCancel?: () => void; 19 | }; 20 | 21 | export type PaystackParams = { 22 | email: string; 23 | amount: number; 24 | metadata?: Record; 25 | reference?: string; 26 | plan?: string; 27 | invoice_limit?: number; 28 | subaccount?: string; 29 | split_code?: string; 30 | split?: DynamicMultiSplitProps; 31 | onSuccess: (data: PaystackTransactionResponse) => void; 32 | onCancel: () => void; 33 | onLoad?: (res: PaystackOnloadResponse) => void; 34 | onError?: (res: any) => void; 35 | }; 36 | 37 | export type PaystackCheckoutParams = { 38 | email: string; 39 | amount: number; 40 | reference?: string; 41 | metadata?: Record; 42 | plan?: string; 43 | invoice_limit?: number; 44 | subaccount?: string; 45 | split_code?: string; 46 | split?: DynamicMultiSplitProps; 47 | onSuccess: (res: SuccessResponse) => void; 48 | onCancel: (res: Response) => void; 49 | onLoad?: (res: { id: string; accessCode: string; customer: Record }) => void; 50 | onError?: (err: { message: string }) => void; 51 | }; 52 | 53 | export interface Response { 54 | status: string; 55 | data?: string; 56 | } 57 | 58 | export interface SuccessResponse extends Response { 59 | transactionRef?: string; 60 | data?: any; 61 | } 62 | 63 | export type Currency = 'NGN' | 'GHS' | 'USD' | 'ZAR' | 'KES' | 'XOF'; 64 | 65 | export type PaymentChannels = ('bank' | 'card' | 'qr' | 'ussd' | 'mobile_money' | 'bank_transfer' | 'eft' | 'apple_pay')[]; 66 | 67 | export type SplitTypes = 'flat' | 'percentage'; 68 | 69 | export type ChargeBearerTypes = 'all' | 'all-proportional' | 'account' | 'subaccount'; 70 | 71 | interface DynamicSplitSubAccountInterface { 72 | subaccount: string; 73 | share: string; 74 | } 75 | 76 | export interface DynamicMultiSplitProps { 77 | type: SplitTypes; 78 | bearer_type: ChargeBearerTypes; 79 | subaccounts: DynamicSplitSubAccountInterface[]; 80 | bearer_subaccount?: string; 81 | reference?: string; 82 | } 83 | -------------------------------------------------------------------------------- /__tests__/index.test.tsx: -------------------------------------------------------------------------------- 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 | email: 'test@example.com', 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 | email: 'test@example.com', 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 | email: 'test@example.com', 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 | email: 'email@test.com', 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@test.com'"); 84 | expect(js).toContain("amount: 10000"); 85 | }); 86 | }); 87 | }); -------------------------------------------------------------------------------- /development/PaystackProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useCallback, useMemo, useState } from 'react'; 2 | import { Modal, SafeAreaView, 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 = ({ 19 | publicKey, 20 | currency, 21 | defaultChannels = ['card'], 22 | debug = false, 23 | children, 24 | onGlobalSuccess, 25 | onGlobalCancel, 26 | }) => { 27 | const [visible, setVisible] = useState(false); 28 | const [params, setParams] = useState(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 && { currency }), 73 | channels: defaultChannels, 74 | plan: params.plan, 75 | invoice_limit: params.invoice_limit, 76 | subaccount: params.subaccount, 77 | split: params.split, 78 | split_code: params.split_code, 79 | }), 80 | method 81 | ); 82 | }, [params, method]); 83 | 84 | if (debug && visible) { 85 | console.log('[Paystack] HTML Injected:', paystackHTML); 86 | } 87 | 88 | return ( 89 | 90 | {children} 91 | 92 | 93 | debug && console.log('[Paystack] WebView Load Start')} 101 | onLoadEnd={() => debug && console.log('[Paystack] WebView Load End')} 102 | renderLoading={() => } 103 | /> 104 | 105 | 106 | 107 | ); 108 | }; -------------------------------------------------------------------------------- /development/utils.ts: -------------------------------------------------------------------------------- 1 | import { Alert } from 'react-native'; 2 | import { Currency, DynamicMultiSplitProps, PaymentChannels, PaystackParams, PaystackTransactionResponse } from './types'; 3 | 4 | export const validateParams = (params: PaystackParams, debug: boolean): boolean => { 5 | const errors: string[] = []; 6 | if (!params.email) errors.push('Email is required'); 7 | if (!params.amount || typeof params.amount !== 'number' || params.amount <= 0) { 8 | errors.push('Amount must be a valid number greater than 0'); 9 | } 10 | if (!params.onSuccess || typeof params.onSuccess !== 'function') { 11 | errors.push('onSuccess callback is required and must be a function'); 12 | } 13 | if (!params.onCancel || typeof params.onCancel !== 'function') { 14 | errors.push('onCancel callback is required and must be a function'); 15 | } 16 | 17 | if (errors.length > 0) { 18 | debug && console.warn('Paystack Validation Errors:', errors); 19 | Alert.alert('Payment Error', errors.join('\n')); 20 | return false; 21 | } 22 | return true; 23 | }; 24 | 25 | export const sanitize = ( 26 | value: unknown, 27 | fallback: string | number | object, 28 | wrapString = true 29 | ): string => { 30 | try { 31 | if (typeof value === 'string') return wrapString ? `'${value}'` : value; 32 | return JSON.stringify(value ?? fallback); 33 | } catch (e) { 34 | return JSON.stringify(fallback); 35 | } 36 | }; 37 | 38 | export const handlePaystackMessage = ({ 39 | event, 40 | debug, 41 | params, 42 | onGlobalSuccess, 43 | onGlobalCancel, 44 | close, 45 | }: { 46 | event: any; 47 | debug: boolean; 48 | params: PaystackParams | null; 49 | onGlobalSuccess?: (data: PaystackTransactionResponse) => void; 50 | onGlobalCancel?: () => void; 51 | close?: () => void; 52 | }) => { 53 | try { 54 | const data = JSON.parse(event.nativeEvent.data); 55 | if (debug) console.log('[Paystack] Message Received:', data); 56 | 57 | switch (data.event) { 58 | case 'success': { 59 | if (debug) console.log('[Paystack] Success:', data.data); 60 | params?.onSuccess(data.data); 61 | onGlobalSuccess?.(data.data); 62 | close?.(); 63 | break; 64 | } 65 | case 'cancel': { 66 | if (debug) console.log('[Paystack] Cancelled'); 67 | params?.onCancel(); 68 | onGlobalCancel?.(); 69 | close?.(); 70 | break; 71 | } 72 | case 'error': { 73 | if (debug) console.error('[Paystack] Error:', data.error); 74 | close?.(); 75 | break; 76 | } 77 | case 'load': { 78 | if (debug) console.log('[Paystack] Loaded:', data); 79 | break; 80 | } 81 | } 82 | } catch (e) { 83 | if (debug) console.warn('[Paystack] Message Error:', e); 84 | } 85 | }; 86 | 87 | export const generatePaystackParams = (config: { 88 | publicKey: string; 89 | email: string; 90 | amount: number; 91 | reference: string; 92 | metadata?: object; 93 | currency?: Currency; 94 | channels: PaymentChannels; 95 | plan?: string; 96 | invoice_limit?: number; 97 | subaccount?: string; 98 | split_code?: string; 99 | split?: DynamicMultiSplitProps; 100 | }): string => { 101 | const props = [ 102 | `key: '${config.publicKey}'`, 103 | `email: '${config.email}'`, 104 | `amount: ${config.amount * 100}`, 105 | config.currency ? `currency: '${config.currency}'` : '', 106 | `reference: '${config.reference}'`, 107 | config.metadata ? `metadata: ${JSON.stringify(config.metadata)}` : '', 108 | config.channels ? `channels: ${JSON.stringify(config.channels)}` : '', 109 | config.plan ? `plan: '${config.plan}'` : '', 110 | config.invoice_limit ? `invoice_limit: ${config.invoice_limit}` : '', 111 | config.subaccount ? `subaccount: '${config.subaccount}'` : '', 112 | config.split_code ? `split_code: '${config.split_code}'` : '', 113 | config.split ? `split: ${JSON.stringify(config.split)}` : '', 114 | `onSuccess: function(response) { 115 | window.ReactNativeWebView.postMessage(JSON.stringify({ event: 'success', data: response })); 116 | }`, 117 | `onCancel: function() { 118 | window.ReactNativeWebView.postMessage(JSON.stringify({ event: 'cancel' })); 119 | }`, 120 | `onLoad: function(response) { 121 | window.ReactNativeWebView.postMessage(JSON.stringify({ event: 'load', data: response })); 122 | }`, 123 | `onError: function(error) { 124 | window.ReactNativeWebView.postMessage(JSON.stringify({ event: 'error', error: { message: error.message } })); 125 | }` 126 | ]; 127 | 128 | return props.filter(Boolean).join(',\n'); 129 | }; 130 | 131 | export const paystackHtmlContent = ( 132 | params: string, 133 | method: 'checkout' | 'newTransaction' = 'checkout' 134 | ): string => ` 135 | 136 | 137 | 138 | 139 | 140 | Paystack 141 | 142 | 143 | 144 | 152 | 153 | 154 | `; 155 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "just1and0", 10 | "name": "Oluwatobi Shokunbi ", 11 | "avatar_url": "https://avatars3.githubusercontent.com/u/17249207?v=4", 12 | "profile": "https://linksnest.com/just1and0", 13 | "contributions": [ 14 | "code", 15 | "doc" 16 | ] 17 | }, 18 | { 19 | "login": "mosoakinyemi", 20 | "name": "Akinyemi Mosolasi", 21 | "avatar_url": "https://avatars2.githubusercontent.com/u/41248079?v=4", 22 | "profile": "https://github.com/mosoakinyemi", 23 | "contributions": [ 24 | "doc", 25 | "code" 26 | ] 27 | }, 28 | { 29 | "login": "okechukwu0127", 30 | "name": "okechukwu0127", 31 | "avatar_url": "https://avatars0.githubusercontent.com/u/23473673?v=4", 32 | "profile": "https://github.com/okechukwu0127", 33 | "contributions": [ 34 | "code", 35 | "bug" 36 | ] 37 | }, 38 | { 39 | "login": "johneyo", 40 | "name": "Johney", 41 | "avatar_url": "https://avatars2.githubusercontent.com/u/36991140?v=4", 42 | "profile": "https://github.com/johneyo", 43 | "contributions": [ 44 | "code" 45 | ] 46 | }, 47 | { 48 | "login": "samie820", 49 | "name": "sammy", 50 | "avatar_url": "https://avatars2.githubusercontent.com/u/27306463?v=4", 51 | "profile": "https://twitter.com/AjeboDeveloper", 52 | "contributions": [ 53 | "code" 54 | ] 55 | }, 56 | { 57 | "login": "walexanderos", 58 | "name": "Jimoh Jamiu", 59 | "avatar_url": "https://avatars0.githubusercontent.com/u/36700043?v=4", 60 | "profile": "https://github.com/walexanderos", 61 | "contributions": [ 62 | "bug", 63 | "doc", 64 | "code" 65 | ] 66 | }, 67 | { 68 | "login": "cahakgeorge", 69 | "name": "Cahak George", 70 | "avatar_url": "https://avatars3.githubusercontent.com/u/8522701?v=4", 71 | "profile": "https://medium.com/@cahakgeorge", 72 | "contributions": [ 73 | "code" 74 | ] 75 | }, 76 | { 77 | "login": "johnayeni", 78 | "name": "John Ayeni", 79 | "avatar_url": "https://avatars0.githubusercontent.com/u/22295092?v=4", 80 | "profile": "https://johnayeni.xyz", 81 | "contributions": [ 82 | "doc" 83 | ] 84 | }, 85 | { 86 | "login": "majirieyowel", 87 | "name": "majirieyowel", 88 | "avatar_url": "https://avatars.githubusercontent.com/u/30162976?v=4", 89 | "profile": "https://github.com/majirieyowel", 90 | "contributions": [ 91 | "code" 92 | ] 93 | }, 94 | { 95 | "login": "Zeusmist", 96 | "name": "David Erinayo Obidu", 97 | "avatar_url": "https://avatars.githubusercontent.com/u/51177741?v=4", 98 | "profile": "https://github.com/Zeusmist", 99 | "contributions": [ 100 | "bug" 101 | ] 102 | }, 103 | { 104 | "login": "surafelbm", 105 | "name": "surafelbm", 106 | "avatar_url": "https://avatars.githubusercontent.com/u/11531221?v=4", 107 | "profile": "https://github.com/surafelbm", 108 | "contributions": [ 109 | "bug" 110 | ] 111 | }, 112 | { 113 | "login": "omivrex", 114 | "name": "Rex Omiv", 115 | "avatar_url": "https://avatars.githubusercontent.com/u/42608841?v=4", 116 | "profile": "https://github.com/omivrex", 117 | "contributions": [ 118 | "bug" 119 | ] 120 | }, 121 | { 122 | "login": "ossyfizy1", 123 | "name": "Osagie Osaigbovo Charles", 124 | "avatar_url": "https://avatars.githubusercontent.com/u/18512476?v=4", 125 | "profile": "https://github.com/ossyfizy1", 126 | "contributions": [ 127 | "bug" 128 | ] 129 | }, 130 | { 131 | "login": "Ujjalcha1", 132 | "name": "Ujjalcha1", 133 | "avatar_url": "https://avatars.githubusercontent.com/u/40722840?v=4", 134 | "profile": "https://github.com/Ujjalcha1", 135 | "contributions": [ 136 | "bug" 137 | ] 138 | }, 139 | { 140 | "login": "Blac-Panda", 141 | "name": "Oyefeso Oluwatunmise", 142 | "avatar_url": "https://avatars.githubusercontent.com/u/23406970?v=4", 143 | "profile": "http://codexplorer.me", 144 | "contributions": [ 145 | "bug" 146 | ] 147 | }, 148 | { 149 | "login": "fuadop", 150 | "name": "Fuad Olatunji", 151 | "avatar_url": "https://avatars.githubusercontent.com/u/65264054?v=4", 152 | "profile": "http://fuadolatunji.me", 153 | "contributions": [ 154 | "doc" 155 | ] 156 | }, 157 | { 158 | "login": "erasmuswill", 159 | "name": "Wilhelm Erasmus", 160 | "avatar_url": "https://avatars.githubusercontent.com/u/15966713?v=4", 161 | "profile": "https://erasmuswill.dev", 162 | "contributions": [ 163 | "code" 164 | ] 165 | }, 166 | { 167 | "login": "opmat", 168 | "name": "Matiluko Opeyemi Emmanuel", 169 | "avatar_url": "https://avatars.githubusercontent.com/u/2133903?v=4", 170 | "profile": "https://github.com/opmat", 171 | "contributions": [ 172 | "code", 173 | "doc" 174 | ] 175 | }, 176 | { 177 | "login": "mureyvenom", 178 | "name": "Oluwamurewa Alao", 179 | "avatar_url": "https://avatars.githubusercontent.com/u/47125673?v=4", 180 | "profile": "https://mureyfolio.com.ng/", 181 | "contributions": [ 182 | "code" 183 | ] 184 | }, 185 | { 186 | "login": "El-Nazy", 187 | "name": "Emmanuel Ngene", 188 | "avatar_url": "https://avatars.githubusercontent.com/u/48170272?v=4", 189 | "profile": "https://github.com/El-Nazy", 190 | "contributions": [ 191 | "code" 192 | ] 193 | }, 194 | { 195 | "login": "maukoese", 196 | "name": "Mauko", 197 | "avatar_url": "https://avatars.githubusercontent.com/u/14233942?v=4", 198 | "profile": "https://osen.co.ke", 199 | "contributions": [ 200 | "doc" 201 | ] 202 | }, 203 | { 204 | "login": "quartusk", 205 | "name": "Quartus Kok", 206 | "avatar_url": "https://avatars.githubusercontent.com/u/39792290?v=4", 207 | "profile": "https://github.com/quartusk", 208 | "contributions": [ 209 | "code" 210 | ] 211 | }, 212 | { 213 | "login": "tolu-paystack", 214 | "name": "Tolu Kalejaiye", 215 | "avatar_url": "https://avatars.githubusercontent.com/u/60101507?v=4", 216 | "profile": "https://github.com/tolu-paystack", 217 | "contributions": [ 218 | "code", 219 | "bug" 220 | ] 221 | }, 222 | { 223 | "login": "Harithmetic1", 224 | "name": "Harith Onigemo ", 225 | "avatar_url": "https://avatars.githubusercontent.com/u/45686269?v=4", 226 | "profile": "https://harith-onigemo.netlify.app/", 227 | "contributions": [ 228 | "code" 229 | ] 230 | } 231 | ], 232 | "contributorsPerLine": 7, 233 | "projectName": "React-Native-Paystack-WebView", 234 | "projectOwner": "just1and0", 235 | "repoType": "github", 236 | "repoHost": "https://github.com", 237 | "skipCi": true, 238 | "commitType": "docs", 239 | "commitConvention": "angular" 240 | } 241 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |

React Native Paystack WebView (v5)

5 | 6 |

7 | Modern, hook-based, Paystack-powered payments in React Native apps using WebViews — now streamlined with Provider architecture & fully customizable. 8 |

9 | 10 | Endorsed by Paystack, so you know you’re in good hands. Payment processing has never been this easy! 11 | 12 | All Contributors 13 | 14 | 15 |
16 | 17 |
18 | Screenshot of library in action 19 |
20 | 21 | --- 22 | 23 | 24 | ## 🚀 Installation 25 | 26 | ```bash 27 | npm install react-native-paystack-webview 28 | # or 29 | yarn add react-native-paystack-webview 30 | ``` 31 | 32 | ### 📦 Peer Dependency 33 | 34 | ```bash 35 | yarn add react-native-webview 36 | 37 | # iOS 38 | cd ios && pod install 39 | 40 | # Expo 41 | npx expo install react-native-webview 42 | ``` 43 | 44 | --- 45 | 46 | ## ⚡ Quick Start 47 | 48 | ### Wrap your app with the Provider 49 | 50 | ```tsx 51 | import { PaystackProvider } from 'react-native-paystack-webview'; 52 | 53 | 54 | 55 | 56 | ``` 57 | 58 | ### Use in a component 59 | 60 | ```tsx 61 | import React from 'react'; 62 | import { Button } from 'react-native'; 63 | import { usePaystack } from 'react-native-paystack-webview'; 64 | 65 | const Checkout = () => { 66 | const { popup } = usePaystack(); 67 | 68 | const payNow = () => { 69 | popup.checkout({ 70 | email: 'jane.doe@example.com', 71 | amount: 5000, 72 | reference: 'TXN_123456', 73 | plan: 'PLN_example123', 74 | invoice_limit: 3, 75 | subaccount: 'SUB_abc123', 76 | split_code: 'SPL_def456', 77 | split: { 78 | type: 'percentage', 79 | bearer_type: 'account', 80 | subaccounts: [ 81 | { subaccount: 'ACCT_abc', share: 60 }, 82 | { subaccount: 'ACCT_xyz', share: 40 } 83 | ] 84 | }, 85 | metadata: { 86 | custom_fields: [ 87 | { 88 | display_name: 'Order ID', 89 | variable_name: 'order_id', 90 | value: 'OID1234' 91 | } 92 | ] 93 | }, 94 | onSuccess: (res) => console.log('Success:', res), 95 | onCancel: () => console.log('User cancelled'), 96 | onLoad: (res) => console.log('WebView Loaded:', res), 97 | onError: (err) => console.log('WebView Error:', err) 98 | }); 99 | }; 100 | 101 | return