react-native-fincra-checkout 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +309 -0
- package/lib/commonjs/checkout/FincraCheckout.js +249 -0
- package/lib/commonjs/components/FincraInlineCheckout.js +385 -0
- package/lib/commonjs/components/FincraWebViewCheckout.js +300 -0
- package/lib/commonjs/index.js +23 -0
- package/lib/commonjs/inline/JsBridge.js +91 -0
- package/lib/commonjs/inline/htmlGenerator.js +111 -0
- package/lib/commonjs/types/index.js +27 -0
- package/lib/commonjs/utils/UrlHandler.js +113 -0
- package/lib/typescript/checkout/FincraCheckout.d.ts +121 -0
- package/lib/typescript/components/FincraInlineCheckout.d.ts +21 -0
- package/lib/typescript/components/FincraWebViewCheckout.d.ts +18 -0
- package/lib/typescript/index.d.ts +5 -0
- package/lib/typescript/inline/JsBridge.d.ts +33 -0
- package/lib/typescript/inline/htmlGenerator.d.ts +21 -0
- package/lib/typescript/types/index.d.ts +142 -0
- package/lib/typescript/utils/UrlHandler.d.ts +44 -0
- package/package.json +113 -0
- package/src/checkout/FincraCheckout.tsx +329 -0
- package/src/components/FincraInlineCheckout.tsx +490 -0
- package/src/components/FincraWebViewCheckout.tsx +415 -0
- package/src/index.ts +31 -0
- package/src/inline/JsBridge.ts +108 -0
- package/src/inline/htmlGenerator.ts +138 -0
- package/src/types/index.ts +165 -0
- package/src/utils/UrlHandler.ts +124 -0
package/README.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# react-native-fincra-checkout
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://img.shields.io/npm/v/react-native-fincra-checkout?color=0066FF&style=flat-square" alt="npm version" />
|
|
5
|
+
<img src="https://img.shields.io/badge/TypeScript-100%25-blue?style=flat-square" alt="TypeScript" />
|
|
6
|
+
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license" />
|
|
7
|
+
<img src="https://img.shields.io/badge/platform-iOS%20%7C%20Android-lightgrey?style=flat-square" alt="platforms" />
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
A **production-ready**, **100% TypeScript** React Native SDK for [Fincra Checkout](https://fincra.com/checkout), with full feature and architectural parity with the official `flutter_fincra_checkout` package.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- ✅ **Two checkout modes**: WebView (recommended) and Inline JavaScript
|
|
17
|
+
- ✅ **Imperative API**: `await FincraCheckout.openWebView({...})` from anywhere
|
|
18
|
+
- ✅ **Declarative API**: `<FincraWebViewCheckout />` and `<FincraInlineCheckout />`
|
|
19
|
+
- ✅ **Strongly-typed result**: Discriminated union — `success | error | cancelled`
|
|
20
|
+
- ✅ **URL interception**: Redirect URL prefix match + query-param fallback
|
|
21
|
+
- ✅ **15-second init timeout** for the Inline mode
|
|
22
|
+
- ✅ **Modern SafeAreaView** via `react-native-safe-area-context`
|
|
23
|
+
- ✅ **Built-in Error Recovery & Offline Retry UI** with custom `renderError` prop support
|
|
24
|
+
- ✅ **Android back button** support
|
|
25
|
+
- ✅ **Cancellation confirmation dialog** (optional)
|
|
26
|
+
- ✅ **XSS-safe** HTML generation (all inputs JSON-encoded)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install react-native-fincra-checkout react-native-webview react-native-safe-area-context
|
|
34
|
+
# or
|
|
35
|
+
yarn add react-native-fincra-checkout react-native-webview react-native-safe-area-context
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### iOS — link native modules
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cd ios && pod install
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Android — no extra steps needed
|
|
45
|
+
|
|
46
|
+
`react-native-webview` auto-links on Android.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## ⚠️ Security Notice
|
|
51
|
+
|
|
52
|
+
> **Never store your Fincra Secret Key in your mobile app bundle.**
|
|
53
|
+
>
|
|
54
|
+
> - For **WebView Checkout**: Generate the `checkoutUrl` server-side using your secret key via the Fincra API, then pass the URL to the SDK.
|
|
55
|
+
> - For **Inline Checkout**: Only your **public key** (`pk_...`) is used. This is safe to bundle.
|
|
56
|
+
>
|
|
57
|
+
> Storing secret keys in client code exposes them to reverse engineering and can lead to fraudulent transactions.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Setup — Add the Host Component
|
|
62
|
+
|
|
63
|
+
Add `<FincraCheckoutHost />` **once** at your app root. This enables the imperative `FincraCheckout.open*()` API:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// App.tsx
|
|
67
|
+
import { FincraCheckoutHost } from 'react-native-fincra-checkout';
|
|
68
|
+
|
|
69
|
+
export default function App() {
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
<NavigationContainer>
|
|
73
|
+
<RootNavigator />
|
|
74
|
+
</NavigationContainer>
|
|
75
|
+
|
|
76
|
+
{/* ← Add this once at the end of your root component */}
|
|
77
|
+
<FincraCheckoutHost />
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
> **Note**: The host renders nothing until a checkout is opened. It must be inside a rendered component tree (not a provider).
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## WebView vs. Inline — Comparison
|
|
88
|
+
|
|
89
|
+
| Feature | WebView Checkout | Inline JS Checkout |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| **Trigger** | Backend-generated URL | Public key + params |
|
|
92
|
+
| **Key required** | Secret key *(server-side only)* | Public key *(client-safe)* |
|
|
93
|
+
| **Payment flow** | Full Fincra-hosted page | Embedded Fincra JS widget |
|
|
94
|
+
| **URL interception** | ✅ Redirect URL or query params | ❌ N/A (JS bridge events) |
|
|
95
|
+
| **Init timeout** | ❌ N/A | ✅ 15 seconds |
|
|
96
|
+
| **Recommended for** | Production (most secure) | Frontend-only prototypes |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Usage
|
|
101
|
+
|
|
102
|
+
### A. Imperative API (Promise / async-await)
|
|
103
|
+
|
|
104
|
+
#### WebView Mode — recommended
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { FincraCheckout } from 'react-native-fincra-checkout';
|
|
108
|
+
|
|
109
|
+
async function handlePayment() {
|
|
110
|
+
const result = await FincraCheckout.openWebView({
|
|
111
|
+
// Generated by your backend using Fincra API + secret key
|
|
112
|
+
checkoutUrl: 'https://checkout.fincra.com/pay/abc123',
|
|
113
|
+
// Your backend redirect URL — intercepted by the SDK
|
|
114
|
+
redirectUrl: 'https://api.yourapp.com/payment/callback',
|
|
115
|
+
headerTitle: 'Complete Payment',
|
|
116
|
+
showCancelConfirmationDialog: true,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
switch (result.type) {
|
|
120
|
+
case 'success':
|
|
121
|
+
console.log('Payment successful:', result.response.reference);
|
|
122
|
+
break;
|
|
123
|
+
case 'error':
|
|
124
|
+
console.error('Payment failed:', result.error.message);
|
|
125
|
+
break;
|
|
126
|
+
case 'cancelled':
|
|
127
|
+
console.log('User cancelled the payment');
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### Inline Mode
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { FincraCheckout } from 'react-native-fincra-checkout';
|
|
137
|
+
|
|
138
|
+
async function handleInlinePayment() {
|
|
139
|
+
const result = await FincraCheckout.openInline({
|
|
140
|
+
publicKey: 'pk_live_xxxxxxxxxxxx',
|
|
141
|
+
amount: 5000, // in smallest currency unit (e.g., kobo for NGN)
|
|
142
|
+
currency: 'NGN',
|
|
143
|
+
customerEmail: 'customer@example.com',
|
|
144
|
+
customerName: 'Jane Doe',
|
|
145
|
+
customerPhoneNumber: '08012345678',
|
|
146
|
+
feeBearer: 'customer',
|
|
147
|
+
reference: 'ORDER-001', // optional — Fincra generates one if omitted
|
|
148
|
+
paymentMethods: ['card', 'bank_transfer'], // optional
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (result.type === 'success') {
|
|
152
|
+
const { reference, transactionId, status } = result.response;
|
|
153
|
+
console.log({ reference, transactionId, status });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### B. Declarative Component API
|
|
161
|
+
|
|
162
|
+
Embed checkout views directly inside your own modals, bottom sheets, or navigation screens:
|
|
163
|
+
|
|
164
|
+
#### `<FincraWebViewCheckout />`
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
import { FincraWebViewCheckout } from 'react-native-fincra-checkout';
|
|
168
|
+
|
|
169
|
+
function PaymentScreen() {
|
|
170
|
+
return (
|
|
171
|
+
<FincraWebViewCheckout
|
|
172
|
+
checkoutUrl="https://checkout.fincra.com/pay/abc123"
|
|
173
|
+
redirectUrl="https://api.yourapp.com/payment/callback"
|
|
174
|
+
headerTitle="Secure Payment"
|
|
175
|
+
headerBackgroundColor="#0066FF"
|
|
176
|
+
headerTintColor="#FFFFFF"
|
|
177
|
+
showCancelConfirmationDialog
|
|
178
|
+
onSuccess={(response) => {
|
|
179
|
+
console.log('Success:', response.reference);
|
|
180
|
+
navigation.navigate('PaymentSuccess');
|
|
181
|
+
}}
|
|
182
|
+
onFailed={(error) => {
|
|
183
|
+
console.error('Error:', error.message);
|
|
184
|
+
}}
|
|
185
|
+
onCancelled={() => {
|
|
186
|
+
navigation.goBack();
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### `<FincraInlineCheckout />`
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
import { FincraInlineCheckout } from 'react-native-fincra-checkout';
|
|
197
|
+
|
|
198
|
+
function InlinePaymentScreen() {
|
|
199
|
+
return (
|
|
200
|
+
<FincraInlineCheckout
|
|
201
|
+
publicKey="pk_live_xxxxxxxxxxxx"
|
|
202
|
+
amount={10000}
|
|
203
|
+
currency="NGN"
|
|
204
|
+
customerEmail="customer@example.com"
|
|
205
|
+
customerName="John Doe"
|
|
206
|
+
customerPhoneNumber="08099887766"
|
|
207
|
+
feeBearer="business"
|
|
208
|
+
onSuccess={(response) => console.log(response)}
|
|
209
|
+
onFailed={(error) => console.error(error)}
|
|
210
|
+
onCancelled={() => navigation.goBack()}
|
|
211
|
+
/>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## TypeScript Types
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import type {
|
|
222
|
+
FincraCheckoutResult,
|
|
223
|
+
FincraPaymentResponse,
|
|
224
|
+
FincraPaymentError,
|
|
225
|
+
WebViewCheckoutConfig,
|
|
226
|
+
InlineCheckoutConfig,
|
|
227
|
+
FincraCurrency,
|
|
228
|
+
FeeBearer,
|
|
229
|
+
} from 'react-native-fincra-checkout';
|
|
230
|
+
|
|
231
|
+
// Discriminated union result
|
|
232
|
+
const result: FincraCheckoutResult =
|
|
233
|
+
| { type: 'success'; response: FincraPaymentResponse }
|
|
234
|
+
| { type: 'error'; error: FincraPaymentError }
|
|
235
|
+
| { type: 'cancelled' };
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Supported Currencies
|
|
239
|
+
|
|
240
|
+
`NGN` · `USD` · `GBP` · `EUR` · `GHS` · `KES` · `ZAR` · `UGX` · `XAF` · `XOF`
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Props Reference
|
|
245
|
+
|
|
246
|
+
### Shared (`BaseCheckoutProps`)
|
|
247
|
+
|
|
248
|
+
| Prop | Type | Default | Description |
|
|
249
|
+
|---|---|---|---|
|
|
250
|
+
| `onSuccess` | `(response) => void` | — | Called on successful payment |
|
|
251
|
+
| `onFailed` | `(error) => void` | — | Called on payment error |
|
|
252
|
+
| `onCancelled` | `() => void` | — | Called when user cancels |
|
|
253
|
+
| `headerTitle` | `string` | `'Secure Checkout'` | Navigation bar title |
|
|
254
|
+
| `headerBackgroundColor` | `string` | `'#FFFFFF'` | Nav bar background color |
|
|
255
|
+
| `headerTintColor` | `string` | `'#000000'` | Nav bar text/icon color |
|
|
256
|
+
| `showCancelConfirmationDialog` | `boolean` | `false` | Show Alert before closing |
|
|
257
|
+
| `loadingComponent` | `ReactNode` | `ActivityIndicator` | Custom loading spinner |
|
|
258
|
+
| `closeIcon` | `ReactNode` | `✕` text | Custom close button content |
|
|
259
|
+
|
|
260
|
+
### `WebViewCheckoutConfig`
|
|
261
|
+
|
|
262
|
+
| Prop | Type | Required | Description |
|
|
263
|
+
|---|---|---|---|
|
|
264
|
+
| `checkoutUrl` | `string` | ✅ | Backend-generated Fincra checkout URL |
|
|
265
|
+
| `redirectUrl` | `string` | — | Redirect URL to intercept for completion |
|
|
266
|
+
|
|
267
|
+
### `InlineCheckoutConfig`
|
|
268
|
+
|
|
269
|
+
| Prop | Type | Required | Description |
|
|
270
|
+
|---|---|---|---|
|
|
271
|
+
| `publicKey` | `string` | ✅ | Your Fincra public key (`pk_...`) |
|
|
272
|
+
| `amount` | `number` | ✅ | Amount in smallest currency unit |
|
|
273
|
+
| `currency` | `FincraCurrency` | ✅ | Payment currency |
|
|
274
|
+
| `customerEmail` | `string` | ✅ | Customer email |
|
|
275
|
+
| `customerName` | `string` | ✅ | Customer full name |
|
|
276
|
+
| `customerPhoneNumber` | `string` | ✅ | Customer phone number |
|
|
277
|
+
| `feeBearer` | `FeeBearer` | ✅ | `'business'` or `'customer'` |
|
|
278
|
+
| `reference` | `string` | — | Custom transaction reference |
|
|
279
|
+
| `paymentMethods` | `string[]` | — | Restrict to specific methods |
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## How URL Interception Works
|
|
284
|
+
|
|
285
|
+
The WebView mode intercepts navigation requests:
|
|
286
|
+
|
|
287
|
+
1. **If `redirectUrl` is set**: Any URL starting with `redirectUrl` triggers completion (prefix match — mirrors Flutter's `url.startsWith(redirectUrl)`).
|
|
288
|
+
2. **Fallback** (no `redirectUrl`): Completion is detected when both `status` (or `payment_status`) **and** `reference` query params are present.
|
|
289
|
+
|
|
290
|
+
Response parameters are normalized:
|
|
291
|
+
- `customerReference` → `reference` (preferred)
|
|
292
|
+
- `merchantReference` → `reference` (fallback)
|
|
293
|
+
- `transactionReference` → `transactionId`
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Running Tests
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
npm test
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Tests cover `UrlHandler` (URL detection, param extraction, reference normalization) and `JsBridge` (event parsing, data coercion, malformed input handling) — no device or emulator required.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## License
|
|
308
|
+
|
|
309
|
+
MIT © [Fincra](https://fincra.com)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FincraCheckout = exports.FincraCheckoutHost = void 0;
|
|
37
|
+
exports._registerHostRef = _registerHostRef;
|
|
38
|
+
exports._unregisterHostRef = _unregisterHostRef;
|
|
39
|
+
exports.FincraCheckoutHostRegistrar = FincraCheckoutHostRegistrar;
|
|
40
|
+
const react_1 = __importStar(require("react"));
|
|
41
|
+
const react_native_1 = require("react-native");
|
|
42
|
+
const FincraWebViewCheckout_1 = require("../components/FincraWebViewCheckout");
|
|
43
|
+
const FincraInlineCheckout_1 = require("../components/FincraInlineCheckout");
|
|
44
|
+
/**
|
|
45
|
+
* Place this component once at your app root (inside your root view, after
|
|
46
|
+
* your navigator/providers). It renders nothing until `FincraCheckout.open*()`
|
|
47
|
+
* is called — then it mounts a full-screen `Modal` over the current UI.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* // App.tsx
|
|
52
|
+
* export default function App() {
|
|
53
|
+
* return (
|
|
54
|
+
* <NavigationContainer>
|
|
55
|
+
* <RootNavigator />
|
|
56
|
+
* <FincraCheckoutHost /> {/* ← add this once *\/}
|
|
57
|
+
* </NavigationContainer>
|
|
58
|
+
* );
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
exports.FincraCheckoutHost = (0, react_1.forwardRef)(function FincraCheckoutHost(_props, ref) {
|
|
63
|
+
const [modalState, setModalState] = (0, react_1.useState)({ mode: null });
|
|
64
|
+
const resolveRef = (0, react_1.useRef)(null);
|
|
65
|
+
// ── Resolve and dismiss ────────────────────────────────────────────────────
|
|
66
|
+
const resolve = (0, react_1.useCallback)((result) => {
|
|
67
|
+
setModalState({ mode: null });
|
|
68
|
+
resolveRef.current?.(result);
|
|
69
|
+
resolveRef.current = null;
|
|
70
|
+
}, []);
|
|
71
|
+
// ── Expose imperative methods via ref ──────────────────────────────────────
|
|
72
|
+
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
73
|
+
// Fix #1: guard against double-open — reject instead of orphaning the
|
|
74
|
+
// pending Promise and silently clobbering resolveRef.
|
|
75
|
+
_openWebView(config) {
|
|
76
|
+
if (resolveRef.current) {
|
|
77
|
+
return Promise.reject(new Error('[FincraCheckout] A checkout session is already open. ' +
|
|
78
|
+
'Await the current session before opening another.'));
|
|
79
|
+
}
|
|
80
|
+
return new Promise((res) => {
|
|
81
|
+
resolveRef.current = res;
|
|
82
|
+
setModalState({ mode: 'webview', webViewConfig: config });
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
_openInline(config) {
|
|
86
|
+
if (resolveRef.current) {
|
|
87
|
+
return Promise.reject(new Error('[FincraCheckout] A checkout session is already open. ' +
|
|
88
|
+
'Await the current session before opening another.'));
|
|
89
|
+
}
|
|
90
|
+
return new Promise((res) => {
|
|
91
|
+
resolveRef.current = res;
|
|
92
|
+
setModalState({ mode: 'inline', inlineConfig: config });
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
}), [ /* resolve not needed — used via resolveRef */]);
|
|
96
|
+
const isVisible = modalState.mode !== null;
|
|
97
|
+
// ── Shared callback builders ───────────────────────────────────────────────
|
|
98
|
+
const buildCallbacks = (0, react_1.useCallback)((config) => ({
|
|
99
|
+
onSuccess: (response) => {
|
|
100
|
+
config.onSuccess?.(response);
|
|
101
|
+
resolve({ type: 'success', response });
|
|
102
|
+
},
|
|
103
|
+
onFailed: (error) => {
|
|
104
|
+
config.onFailed?.(error);
|
|
105
|
+
resolve({ type: 'error', error });
|
|
106
|
+
},
|
|
107
|
+
onCancelled: () => {
|
|
108
|
+
config.onCancelled?.();
|
|
109
|
+
resolve({ type: 'cancelled' });
|
|
110
|
+
},
|
|
111
|
+
}), [resolve]);
|
|
112
|
+
return (react_1.default.createElement(react_native_1.Modal, { visible: isVisible, animationType: "slide", presentationStyle: "fullScreen", statusBarTranslucent: true, onRequestClose: () => {
|
|
113
|
+
// Android hardware back — treat as cancellation
|
|
114
|
+
const cfg = modalState.webViewConfig ?? modalState.inlineConfig;
|
|
115
|
+
if (cfg) {
|
|
116
|
+
cfg.onCancelled?.();
|
|
117
|
+
}
|
|
118
|
+
resolve({ type: 'cancelled' });
|
|
119
|
+
} },
|
|
120
|
+
react_1.default.createElement(react_native_1.View, { style: styles.fullscreen },
|
|
121
|
+
modalState.mode === 'webview' && modalState.webViewConfig && (react_1.default.createElement(FincraWebViewCheckout_1.FincraWebViewCheckout, { ...modalState.webViewConfig, ...buildCallbacks(modalState.webViewConfig) })),
|
|
122
|
+
modalState.mode === 'inline' && modalState.inlineConfig && (react_1.default.createElement(FincraInlineCheckout_1.FincraInlineCheckout, { ...modalState.inlineConfig, ...buildCallbacks(modalState.inlineConfig) })))));
|
|
123
|
+
});
|
|
124
|
+
// ─── Singleton Ref ─────────────────────────────────────────────────────────────
|
|
125
|
+
// A module-level ref that FincraCheckout.open*() calls are routed through.
|
|
126
|
+
// Set by the first <FincraCheckoutHost /> that mounts.
|
|
127
|
+
let _hostRef = null;
|
|
128
|
+
/**
|
|
129
|
+
* @internal
|
|
130
|
+
* Called by `<FincraCheckoutHostRegistrar />` to register the singleton ref.
|
|
131
|
+
* Not part of the public API — do not call this directly.
|
|
132
|
+
*/
|
|
133
|
+
function _registerHostRef(ref) {
|
|
134
|
+
_hostRef = ref;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* @internal
|
|
138
|
+
* Clears the singleton ref when the host unmounts.
|
|
139
|
+
* Not part of the public API — do not call this directly.
|
|
140
|
+
*/
|
|
141
|
+
function _unregisterHostRef() {
|
|
142
|
+
_hostRef = null;
|
|
143
|
+
}
|
|
144
|
+
// ─── Public FincraCheckout Static API ─────────────────────────────────────────
|
|
145
|
+
/**
|
|
146
|
+
* Imperative static API for opening Fincra Checkout modals from anywhere
|
|
147
|
+
* in your app — no navigation prop or context required.
|
|
148
|
+
*
|
|
149
|
+
* **Prerequisite**: `<FincraCheckoutHost />` must be mounted at your app root.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* // WebView mode (recommended — backend-generated URL)
|
|
154
|
+
* const result = await FincraCheckout.openWebView({
|
|
155
|
+
* checkoutUrl: 'https://checkout.fincra.com/pay/...',
|
|
156
|
+
* redirectUrl: 'https://api.yourapp.com/payment/callback',
|
|
157
|
+
* });
|
|
158
|
+
*
|
|
159
|
+
* // Inline mode (frontend-initiated)
|
|
160
|
+
* const result = await FincraCheckout.openInline({
|
|
161
|
+
* publicKey: 'pk_live_...',
|
|
162
|
+
* amount: 5000,
|
|
163
|
+
* currency: 'NGN',
|
|
164
|
+
* customerEmail: 'user@example.com',
|
|
165
|
+
* customerName: 'Jane Doe',
|
|
166
|
+
* customerPhoneNumber: '08012345678',
|
|
167
|
+
* feeBearer: 'customer',
|
|
168
|
+
* });
|
|
169
|
+
*
|
|
170
|
+
* switch (result.type) {
|
|
171
|
+
* case 'success': console.log(result.response.reference); break;
|
|
172
|
+
* case 'error': console.error(result.error.message); break;
|
|
173
|
+
* case 'cancelled': console.log('User cancelled'); break;
|
|
174
|
+
* }
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
class FincraCheckout {
|
|
178
|
+
/**
|
|
179
|
+
* Opens the WebView checkout in a full-screen modal.
|
|
180
|
+
*
|
|
181
|
+
* This is the **recommended** flow — your backend generates the `checkoutUrl`
|
|
182
|
+
* using the Fincra API with your **secret key** (never in the app).
|
|
183
|
+
*
|
|
184
|
+
* @param config - WebView checkout configuration.
|
|
185
|
+
* @returns A promise resolving to a `FincraCheckoutResult` discriminated union.
|
|
186
|
+
* @throws Error if `<FincraCheckoutHost />` is not mounted.
|
|
187
|
+
* @throws Error if a checkout session is already open.
|
|
188
|
+
*/
|
|
189
|
+
static openWebView(config) {
|
|
190
|
+
FincraCheckout._assertHostMounted();
|
|
191
|
+
return _hostRef.current._openWebView(config);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Opens the Inline JavaScript checkout in a full-screen modal.
|
|
195
|
+
*
|
|
196
|
+
* Uses only the Fincra **public key** (`pk_...`).
|
|
197
|
+
* The Fincra JS SDK is loaded from the CDN at runtime.
|
|
198
|
+
*
|
|
199
|
+
* @param config - Inline checkout configuration.
|
|
200
|
+
* @returns A promise resolving to a `FincraCheckoutResult` discriminated union.
|
|
201
|
+
* @throws Error if `<FincraCheckoutHost />` is not mounted.
|
|
202
|
+
* @throws Error if a checkout session is already open.
|
|
203
|
+
*/
|
|
204
|
+
static openInline(config) {
|
|
205
|
+
FincraCheckout._assertHostMounted();
|
|
206
|
+
return _hostRef.current._openInline(config);
|
|
207
|
+
}
|
|
208
|
+
static _assertHostMounted() {
|
|
209
|
+
if (!_hostRef?.current) {
|
|
210
|
+
throw new Error('[react-native-fincra-checkout] FincraCheckoutHost is not mounted. ' +
|
|
211
|
+
'Add <FincraCheckoutHost /> to your App root before calling FincraCheckout.open*().');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
exports.FincraCheckout = FincraCheckout;
|
|
216
|
+
// ─── Self-registering Host wrapper ────────────────────────────────────────────
|
|
217
|
+
/**
|
|
218
|
+
* The component you add to your app root.
|
|
219
|
+
* It self-registers as the singleton checkout host.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```tsx
|
|
223
|
+
* // App.tsx
|
|
224
|
+
* import { FincraCheckoutHost } from 'react-native-fincra-checkout';
|
|
225
|
+
*
|
|
226
|
+
* export default function App() {
|
|
227
|
+
* return (
|
|
228
|
+
* <>
|
|
229
|
+
* <YourApp />
|
|
230
|
+
* <FincraCheckoutHost />
|
|
231
|
+
* </>
|
|
232
|
+
* );
|
|
233
|
+
* }
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
function FincraCheckoutHostRegistrar() {
|
|
237
|
+
const ref = (0, react_1.useRef)(null);
|
|
238
|
+
(0, react_1.useEffect)(() => {
|
|
239
|
+
_registerHostRef(ref);
|
|
240
|
+
return () => _unregisterHostRef();
|
|
241
|
+
}, []);
|
|
242
|
+
return react_1.default.createElement(exports.FincraCheckoutHost, { ref: ref });
|
|
243
|
+
}
|
|
244
|
+
// ─── Styles ────────────────────────────────────────────────────────────────────
|
|
245
|
+
const styles = react_native_1.StyleSheet.create({
|
|
246
|
+
fullscreen: {
|
|
247
|
+
flex: 1,
|
|
248
|
+
},
|
|
249
|
+
});
|