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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useImperativeHandle,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { Modal, StyleSheet, View } from 'react-native';
|
|
10
|
+
import type {
|
|
11
|
+
WebViewCheckoutConfig,
|
|
12
|
+
InlineCheckoutConfig,
|
|
13
|
+
FincraCheckoutResult,
|
|
14
|
+
FincraPaymentResponse,
|
|
15
|
+
FincraPaymentError,
|
|
16
|
+
} from '../types';
|
|
17
|
+
import { FincraWebViewCheckout } from '../components/FincraWebViewCheckout';
|
|
18
|
+
import { FincraInlineCheckout } from '../components/FincraInlineCheckout';
|
|
19
|
+
|
|
20
|
+
// ─── Imperative Modal API ─────────────────────────────────────────────────────
|
|
21
|
+
//
|
|
22
|
+
// Mirrors the static FincraCheckout class in fincra_checkout.dart,
|
|
23
|
+
// adapted to React Native's component model using a singleton ref pattern
|
|
24
|
+
// (the same approach used by react-native-toast-message).
|
|
25
|
+
//
|
|
26
|
+
// Usage:
|
|
27
|
+
// 1. Add <FincraCheckoutHost /> to your App root (once, at the top level).
|
|
28
|
+
// 2. Call FincraCheckout.openWebView({...}) / FincraCheckout.openInline({...})
|
|
29
|
+
// from anywhere — no context or navigation prop needed.
|
|
30
|
+
|
|
31
|
+
// ── Internal state types ──────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
type ModalMode = 'webview' | 'inline' | null;
|
|
34
|
+
|
|
35
|
+
// Fix #12: removed the dead `resolve` field — the resolve fn is stored in
|
|
36
|
+
// resolveRef, not in ModalState. Keeping it here only confused readers.
|
|
37
|
+
interface ModalState {
|
|
38
|
+
mode: ModalMode;
|
|
39
|
+
webViewConfig?: WebViewCheckoutConfig;
|
|
40
|
+
inlineConfig?: InlineCheckoutConfig;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Host ref API ──────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @internal
|
|
47
|
+
* Imperative handle for the FincraCheckoutHost ref.
|
|
48
|
+
* Do not call `_openWebView` / `_openInline` directly — use `FincraCheckout.open*()`.
|
|
49
|
+
*/
|
|
50
|
+
export interface FincraCheckoutHostHandle {
|
|
51
|
+
_openWebView(
|
|
52
|
+
config: WebViewCheckoutConfig
|
|
53
|
+
): Promise<FincraCheckoutResult>;
|
|
54
|
+
_openInline(
|
|
55
|
+
config: InlineCheckoutConfig
|
|
56
|
+
): Promise<FincraCheckoutResult>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Place this component once at your app root (inside your root view, after
|
|
61
|
+
* your navigator/providers). It renders nothing until `FincraCheckout.open*()`
|
|
62
|
+
* is called — then it mounts a full-screen `Modal` over the current UI.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* // App.tsx
|
|
67
|
+
* export default function App() {
|
|
68
|
+
* return (
|
|
69
|
+
* <NavigationContainer>
|
|
70
|
+
* <RootNavigator />
|
|
71
|
+
* <FincraCheckoutHost /> {/* ← add this once *\/}
|
|
72
|
+
* </NavigationContainer>
|
|
73
|
+
* );
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export const FincraCheckoutHost = forwardRef<FincraCheckoutHostHandle>(
|
|
78
|
+
function FincraCheckoutHost(_props, ref) {
|
|
79
|
+
const [modalState, setModalState] = useState<ModalState>({ mode: null });
|
|
80
|
+
const resolveRef = useRef<((result: FincraCheckoutResult) => void) | null>(
|
|
81
|
+
null
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// ── Resolve and dismiss ────────────────────────────────────────────────────
|
|
85
|
+
const resolve = useCallback((result: FincraCheckoutResult) => {
|
|
86
|
+
setModalState({ mode: null });
|
|
87
|
+
resolveRef.current?.(result);
|
|
88
|
+
resolveRef.current = null;
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
// ── Expose imperative methods via ref ──────────────────────────────────────
|
|
92
|
+
useImperativeHandle(
|
|
93
|
+
ref,
|
|
94
|
+
() => ({
|
|
95
|
+
// Fix #1: guard against double-open — reject instead of orphaning the
|
|
96
|
+
// pending Promise and silently clobbering resolveRef.
|
|
97
|
+
_openWebView(config: WebViewCheckoutConfig): Promise<FincraCheckoutResult> {
|
|
98
|
+
if (resolveRef.current) {
|
|
99
|
+
return Promise.reject(
|
|
100
|
+
new Error(
|
|
101
|
+
'[FincraCheckout] A checkout session is already open. ' +
|
|
102
|
+
'Await the current session before opening another.'
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return new Promise((res) => {
|
|
107
|
+
resolveRef.current = res;
|
|
108
|
+
setModalState({ mode: 'webview', webViewConfig: config });
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
_openInline(config: InlineCheckoutConfig): Promise<FincraCheckoutResult> {
|
|
112
|
+
if (resolveRef.current) {
|
|
113
|
+
return Promise.reject(
|
|
114
|
+
new Error(
|
|
115
|
+
'[FincraCheckout] A checkout session is already open. ' +
|
|
116
|
+
'Await the current session before opening another.'
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return new Promise((res) => {
|
|
121
|
+
resolveRef.current = res;
|
|
122
|
+
setModalState({ mode: 'inline', inlineConfig: config });
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
[/* resolve not needed — used via resolveRef */]
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const isVisible = modalState.mode !== null;
|
|
130
|
+
|
|
131
|
+
// ── Shared callback builders ───────────────────────────────────────────────
|
|
132
|
+
const buildCallbacks = useCallback(
|
|
133
|
+
(config: WebViewCheckoutConfig | InlineCheckoutConfig) => ({
|
|
134
|
+
onSuccess: (response: FincraPaymentResponse) => {
|
|
135
|
+
config.onSuccess?.(response);
|
|
136
|
+
resolve({ type: 'success', response });
|
|
137
|
+
},
|
|
138
|
+
onFailed: (error: FincraPaymentError) => {
|
|
139
|
+
config.onFailed?.(error);
|
|
140
|
+
resolve({ type: 'error', error });
|
|
141
|
+
},
|
|
142
|
+
onCancelled: () => {
|
|
143
|
+
config.onCancelled?.();
|
|
144
|
+
resolve({ type: 'cancelled' });
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
[resolve]
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Modal
|
|
152
|
+
visible={isVisible}
|
|
153
|
+
animationType="slide"
|
|
154
|
+
presentationStyle="fullScreen"
|
|
155
|
+
statusBarTranslucent
|
|
156
|
+
onRequestClose={() => {
|
|
157
|
+
// Android hardware back — treat as cancellation
|
|
158
|
+
const cfg =
|
|
159
|
+
modalState.webViewConfig ?? modalState.inlineConfig;
|
|
160
|
+
if (cfg) {
|
|
161
|
+
cfg.onCancelled?.();
|
|
162
|
+
}
|
|
163
|
+
resolve({ type: 'cancelled' });
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<View style={styles.fullscreen}>
|
|
167
|
+
{modalState.mode === 'webview' && modalState.webViewConfig && (
|
|
168
|
+
<FincraWebViewCheckout
|
|
169
|
+
{...modalState.webViewConfig}
|
|
170
|
+
{...buildCallbacks(modalState.webViewConfig)}
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
173
|
+
{modalState.mode === 'inline' && modalState.inlineConfig && (
|
|
174
|
+
<FincraInlineCheckout
|
|
175
|
+
{...modalState.inlineConfig}
|
|
176
|
+
{...buildCallbacks(modalState.inlineConfig)}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
</View>
|
|
180
|
+
</Modal>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// ─── Singleton Ref ─────────────────────────────────────────────────────────────
|
|
186
|
+
// A module-level ref that FincraCheckout.open*() calls are routed through.
|
|
187
|
+
// Set by the first <FincraCheckoutHost /> that mounts.
|
|
188
|
+
let _hostRef: React.RefObject<FincraCheckoutHostHandle | null> | null = null;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @internal
|
|
192
|
+
* Called by `<FincraCheckoutHostRegistrar />` to register the singleton ref.
|
|
193
|
+
* Not part of the public API — do not call this directly.
|
|
194
|
+
*/
|
|
195
|
+
export function _registerHostRef(
|
|
196
|
+
ref: React.RefObject<FincraCheckoutHostHandle | null>
|
|
197
|
+
): void {
|
|
198
|
+
_hostRef = ref;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @internal
|
|
203
|
+
* Clears the singleton ref when the host unmounts.
|
|
204
|
+
* Not part of the public API — do not call this directly.
|
|
205
|
+
*/
|
|
206
|
+
export function _unregisterHostRef(): void {
|
|
207
|
+
_hostRef = null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── Public FincraCheckout Static API ─────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Imperative static API for opening Fincra Checkout modals from anywhere
|
|
214
|
+
* in your app — no navigation prop or context required.
|
|
215
|
+
*
|
|
216
|
+
* **Prerequisite**: `<FincraCheckoutHost />` must be mounted at your app root.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```typescript
|
|
220
|
+
* // WebView mode (recommended — backend-generated URL)
|
|
221
|
+
* const result = await FincraCheckout.openWebView({
|
|
222
|
+
* checkoutUrl: 'https://checkout.fincra.com/pay/...',
|
|
223
|
+
* redirectUrl: 'https://api.yourapp.com/payment/callback',
|
|
224
|
+
* });
|
|
225
|
+
*
|
|
226
|
+
* // Inline mode (frontend-initiated)
|
|
227
|
+
* const result = await FincraCheckout.openInline({
|
|
228
|
+
* publicKey: 'pk_live_...',
|
|
229
|
+
* amount: 5000,
|
|
230
|
+
* currency: 'NGN',
|
|
231
|
+
* customerEmail: 'user@example.com',
|
|
232
|
+
* customerName: 'Jane Doe',
|
|
233
|
+
* customerPhoneNumber: '08012345678',
|
|
234
|
+
* feeBearer: 'customer',
|
|
235
|
+
* });
|
|
236
|
+
*
|
|
237
|
+
* switch (result.type) {
|
|
238
|
+
* case 'success': console.log(result.response.reference); break;
|
|
239
|
+
* case 'error': console.error(result.error.message); break;
|
|
240
|
+
* case 'cancelled': console.log('User cancelled'); break;
|
|
241
|
+
* }
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
export class FincraCheckout {
|
|
245
|
+
/**
|
|
246
|
+
* Opens the WebView checkout in a full-screen modal.
|
|
247
|
+
*
|
|
248
|
+
* This is the **recommended** flow — your backend generates the `checkoutUrl`
|
|
249
|
+
* using the Fincra API with your **secret key** (never in the app).
|
|
250
|
+
*
|
|
251
|
+
* @param config - WebView checkout configuration.
|
|
252
|
+
* @returns A promise resolving to a `FincraCheckoutResult` discriminated union.
|
|
253
|
+
* @throws Error if `<FincraCheckoutHost />` is not mounted.
|
|
254
|
+
* @throws Error if a checkout session is already open.
|
|
255
|
+
*/
|
|
256
|
+
static openWebView(
|
|
257
|
+
config: WebViewCheckoutConfig
|
|
258
|
+
): Promise<FincraCheckoutResult> {
|
|
259
|
+
FincraCheckout._assertHostMounted();
|
|
260
|
+
return _hostRef!.current!._openWebView(config);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Opens the Inline JavaScript checkout in a full-screen modal.
|
|
265
|
+
*
|
|
266
|
+
* Uses only the Fincra **public key** (`pk_...`).
|
|
267
|
+
* The Fincra JS SDK is loaded from the CDN at runtime.
|
|
268
|
+
*
|
|
269
|
+
* @param config - Inline checkout configuration.
|
|
270
|
+
* @returns A promise resolving to a `FincraCheckoutResult` discriminated union.
|
|
271
|
+
* @throws Error if `<FincraCheckoutHost />` is not mounted.
|
|
272
|
+
* @throws Error if a checkout session is already open.
|
|
273
|
+
*/
|
|
274
|
+
static openInline(
|
|
275
|
+
config: InlineCheckoutConfig
|
|
276
|
+
): Promise<FincraCheckoutResult> {
|
|
277
|
+
FincraCheckout._assertHostMounted();
|
|
278
|
+
return _hostRef!.current!._openInline(config);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private static _assertHostMounted(): void {
|
|
282
|
+
if (!_hostRef?.current) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
'[react-native-fincra-checkout] FincraCheckoutHost is not mounted. ' +
|
|
285
|
+
'Add <FincraCheckoutHost /> to your App root before calling FincraCheckout.open*().'
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ─── Self-registering Host wrapper ────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The component you add to your app root.
|
|
295
|
+
* It self-registers as the singleton checkout host.
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```tsx
|
|
299
|
+
* // App.tsx
|
|
300
|
+
* import { FincraCheckoutHost } from 'react-native-fincra-checkout';
|
|
301
|
+
*
|
|
302
|
+
* export default function App() {
|
|
303
|
+
* return (
|
|
304
|
+
* <>
|
|
305
|
+
* <YourApp />
|
|
306
|
+
* <FincraCheckoutHost />
|
|
307
|
+
* </>
|
|
308
|
+
* );
|
|
309
|
+
* }
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
export function FincraCheckoutHostRegistrar() {
|
|
313
|
+
const ref = useRef<FincraCheckoutHostHandle | null>(null);
|
|
314
|
+
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
_registerHostRef(ref as React.RefObject<FincraCheckoutHostHandle | null>);
|
|
317
|
+
return () => _unregisterHostRef();
|
|
318
|
+
}, []);
|
|
319
|
+
|
|
320
|
+
return <FincraCheckoutHost ref={ref} />;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ─── Styles ────────────────────────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
const styles = StyleSheet.create({
|
|
326
|
+
fullscreen: {
|
|
327
|
+
flex: 1,
|
|
328
|
+
},
|
|
329
|
+
});
|