react-native-fincra-checkout 1.0.0 → 1.0.2
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 +58 -10
- package/lib/commonjs/checkout/FincraCheckout.js +41 -45
- package/lib/commonjs/components/FincraInlineCheckout.js +110 -97
- package/lib/commonjs/components/FincraWebViewCheckout.js +99 -72
- package/lib/commonjs/inline/JsBridge.js +21 -15
- package/lib/commonjs/inline/htmlGenerator.js +53 -31
- package/lib/commonjs/utils/UrlHandler.js +62 -3
- package/lib/typescript/checkout/FincraCheckout.d.ts +0 -1
- package/lib/typescript/components/FincraInlineCheckout.d.ts +1 -2
- package/lib/typescript/components/FincraWebViewCheckout.d.ts +1 -1
- package/lib/typescript/inline/JsBridge.d.ts +4 -1
- package/lib/typescript/inline/htmlGenerator.d.ts +30 -0
- package/lib/typescript/types/index.d.ts +14 -3
- package/lib/typescript/utils/UrlHandler.d.ts +30 -1
- package/package.json +4 -4
- package/src/checkout/FincraCheckout.tsx +75 -69
- package/src/components/FincraInlineCheckout.tsx +202 -159
- package/src/components/FincraWebViewCheckout.tsx +184 -138
- package/src/inline/JsBridge.ts +28 -20
- package/src/inline/htmlGenerator.ts +65 -35
- package/src/types/index.ts +14 -3
- package/src/utils/UrlHandler.ts +76 -3
|
@@ -8,13 +8,36 @@ export declare class UrlHandler {
|
|
|
8
8
|
* Returns `true` if the given URL signals a Fincra payment completion.
|
|
9
9
|
*
|
|
10
10
|
* Logic (mirrors Flutter):
|
|
11
|
-
* 1. If `expectedRedirectUrl` is provided,
|
|
11
|
+
* 1. If `expectedRedirectUrl` is provided, the URL must match it strictly —
|
|
12
|
+
* see {@link UrlHandler.matchesRedirectUrl}.
|
|
12
13
|
* 2. Fallback: Fincra appends `status` (or `payment_status`) AND `reference` as query params.
|
|
13
14
|
*
|
|
14
15
|
* @param url - The URL being navigated to.
|
|
15
16
|
* @param expectedRedirectUrl - The redirect URL you registered on your backend.
|
|
16
17
|
*/
|
|
17
18
|
static isCompletionUrl(url: string, expectedRedirectUrl?: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Strictly matches `url` against the expected redirect URL.
|
|
21
|
+
*
|
|
22
|
+
* - Scheme, host (case-insensitive) and port (default ports normalised) must be equal.
|
|
23
|
+
* - The path must equal the expected path or continue it at a `/` boundary
|
|
24
|
+
* (trailing slashes ignored). An empty expected path matches any path.
|
|
25
|
+
* - Query string and fragment are ignored.
|
|
26
|
+
* - Falls back to `startsWith` only if the expected URL cannot be parsed.
|
|
27
|
+
*
|
|
28
|
+
* Prevents lookalike hosts such as `https://google.com.evil.io` matching
|
|
29
|
+
* `https://google.com`.
|
|
30
|
+
*/
|
|
31
|
+
static matchesRedirectUrl(url: string, expectedRedirectUrl: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Reads the payment status from completion params, accepting either
|
|
34
|
+
* `status` or `payment_status`. Returns lower-case.
|
|
35
|
+
*
|
|
36
|
+
* A missing status is treated as `'success'` because the sandbox redirect
|
|
37
|
+
* omits it. **Always verify the payment on your backend** (Fincra API or
|
|
38
|
+
* webhook) before fulfilling an order.
|
|
39
|
+
*/
|
|
40
|
+
static extractStatus(params: Record<string, string>): string;
|
|
18
41
|
/**
|
|
19
42
|
* Extracts all query parameters from the URL as a `Record<string, string>`.
|
|
20
43
|
*
|
|
@@ -36,6 +59,12 @@ export declare class UrlHandler {
|
|
|
36
59
|
* @param status - The raw status string from Fincra.
|
|
37
60
|
*/
|
|
38
61
|
static isSuccessStatus(status: string): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Minimal absolute-URL parser. React Native's `URL` polyfill does not
|
|
64
|
+
* implement `hostname`/`port`, so this is done by hand.
|
|
65
|
+
* Returns `null` if the string is not an absolute `scheme://host` URL.
|
|
66
|
+
*/
|
|
67
|
+
private static _parseUrl;
|
|
39
68
|
/**
|
|
40
69
|
* Parses URL query string into a `URLSearchParams`-like `Map`.
|
|
41
70
|
* Works in React Native (no DOM `URL` API available).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-fincra-checkout",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Production-ready React Native SDK for Fincra Checkout — WebView and Inline JavaScript modes with full TypeScript support.",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/commonjs/index.js",
|
|
@@ -46,14 +46,14 @@
|
|
|
46
46
|
],
|
|
47
47
|
"repository": {
|
|
48
48
|
"type": "git",
|
|
49
|
-
"url": "
|
|
49
|
+
"url": "https://github.com/gaiyadev/react-native-fincra-checkout"
|
|
50
50
|
},
|
|
51
51
|
"author": "Fincra",
|
|
52
52
|
"license": "MIT",
|
|
53
53
|
"bugs": {
|
|
54
|
-
"url": "https://github.com/
|
|
54
|
+
"url": "https://github.com/gaiyadev/react-native-fincra-checkout/issues"
|
|
55
55
|
},
|
|
56
|
-
"homepage": "https://github.com/
|
|
56
|
+
"homepage": "https://github.com/gaiyadev/react-native-fincra-checkout/blob/main/README.md",
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"react": ">=17.0.0",
|
|
59
59
|
"react-native": ">=0.68.0",
|
|
@@ -36,6 +36,8 @@ type ModalMode = 'webview' | 'inline' | null;
|
|
|
36
36
|
// resolveRef, not in ModalState. Keeping it here only confused readers.
|
|
37
37
|
interface ModalState {
|
|
38
38
|
mode: ModalMode;
|
|
39
|
+
/** Unique per open() call — used as the child `key` and to drop stale results. */
|
|
40
|
+
sessionId?: number;
|
|
39
41
|
webViewConfig?: WebViewCheckoutConfig;
|
|
40
42
|
inlineConfig?: InlineCheckoutConfig;
|
|
41
43
|
}
|
|
@@ -80,72 +82,83 @@ export const FincraCheckoutHost = forwardRef<FincraCheckoutHostHandle>(
|
|
|
80
82
|
const resolveRef = useRef<((result: FincraCheckoutResult) => void) | null>(
|
|
81
83
|
null
|
|
82
84
|
);
|
|
85
|
+
// Id of the session that may still settle; null once it has settled.
|
|
86
|
+
const activeSessionRef = useRef<number | null>(null);
|
|
87
|
+
const nextSessionIdRef = useRef(0);
|
|
83
88
|
|
|
84
|
-
// ──
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
// ── Settle and dismiss ─────────────────────────────────────────────────────
|
|
90
|
+
// Single exit point. A session settles at most once: a late callback (e.g. a
|
|
91
|
+
// bridge message after the Android back button closed the modal) is
|
|
92
|
+
// ignored, so the merchant's callbacks never fire twice.
|
|
93
|
+
const settleSession = useCallback(
|
|
94
|
+
(sessionId: number, result: FincraCheckoutResult, notify?: () => void) => {
|
|
95
|
+
if (activeSessionRef.current !== sessionId) return;
|
|
96
|
+
activeSessionRef.current = null;
|
|
97
|
+
const res = resolveRef.current;
|
|
98
|
+
resolveRef.current = null;
|
|
99
|
+
setModalState({ mode: null });
|
|
100
|
+
notify?.();
|
|
101
|
+
res?.(result);
|
|
102
|
+
},
|
|
103
|
+
[setModalState]
|
|
104
|
+
);
|
|
90
105
|
|
|
91
106
|
// ── Expose imperative methods via ref ──────────────────────────────────────
|
|
92
|
-
useImperativeHandle(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
);
|
|
107
|
+
useImperativeHandle(ref, () => {
|
|
108
|
+
// Fix #1: guard against double-open — reject instead of orphaning the
|
|
109
|
+
// pending Promise and silently clobbering resolveRef.
|
|
110
|
+
const open = (next: Omit<ModalState, 'sessionId'>) => {
|
|
111
|
+
if (resolveRef.current) {
|
|
112
|
+
return Promise.reject(
|
|
113
|
+
new Error(
|
|
114
|
+
'[FincraCheckout] A checkout session is already open. ' +
|
|
115
|
+
'Await the current session before opening another.'
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return new Promise<FincraCheckoutResult>((res) => {
|
|
120
|
+
const sessionId = ++nextSessionIdRef.current;
|
|
121
|
+
activeSessionRef.current = sessionId;
|
|
122
|
+
resolveRef.current = res;
|
|
123
|
+
setModalState({ ...next, sessionId });
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
_openWebView: (config: WebViewCheckoutConfig) =>
|
|
128
|
+
open({ mode: 'webview', webViewConfig: config }),
|
|
129
|
+
_openInline: (config: InlineCheckoutConfig) =>
|
|
130
|
+
open({ mode: 'inline', inlineConfig: config }),
|
|
131
|
+
};
|
|
132
|
+
}, [setModalState]);
|
|
128
133
|
|
|
129
134
|
const isVisible = modalState.mode !== null;
|
|
135
|
+
const sessionId = modalState.sessionId ?? -1;
|
|
130
136
|
|
|
131
137
|
// ── Shared callback builders ───────────────────────────────────────────────
|
|
132
|
-
const buildCallbacks =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
138
|
+
const buildCallbacks = (
|
|
139
|
+
config: WebViewCheckoutConfig | InlineCheckoutConfig
|
|
140
|
+
) => ({
|
|
141
|
+
onSuccess: (response: FincraPaymentResponse) =>
|
|
142
|
+
settleSession(sessionId, { type: 'success', response }, () =>
|
|
143
|
+
config.onSuccess?.(response)
|
|
144
|
+
),
|
|
145
|
+
onFailed: (error: FincraPaymentError) =>
|
|
146
|
+
settleSession(sessionId, { type: 'error', error }, () =>
|
|
147
|
+
config.onFailed?.(error)
|
|
148
|
+
),
|
|
149
|
+
onCancelled: () =>
|
|
150
|
+
settleSession(sessionId, { type: 'cancelled' }, () =>
|
|
151
|
+
config.onCancelled?.()
|
|
152
|
+
),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Android hardware back (Modal intercepts it) — settles as cancelled.
|
|
156
|
+
const handleRequestClose = () => {
|
|
157
|
+
const cfg = modalState.webViewConfig ?? modalState.inlineConfig;
|
|
158
|
+
settleSession(sessionId, { type: 'cancelled' }, () =>
|
|
159
|
+
cfg?.onCancelled?.()
|
|
160
|
+
);
|
|
161
|
+
};
|
|
149
162
|
|
|
150
163
|
return (
|
|
151
164
|
<Modal
|
|
@@ -153,25 +166,19 @@ export const FincraCheckoutHost = forwardRef<FincraCheckoutHostHandle>(
|
|
|
153
166
|
animationType="slide"
|
|
154
167
|
presentationStyle="fullScreen"
|
|
155
168
|
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
|
-
}}
|
|
169
|
+
onRequestClose={handleRequestClose}
|
|
165
170
|
>
|
|
166
171
|
<View style={styles.fullscreen}>
|
|
167
172
|
{modalState.mode === 'webview' && modalState.webViewConfig && (
|
|
168
173
|
<FincraWebViewCheckout
|
|
174
|
+
key={sessionId}
|
|
169
175
|
{...modalState.webViewConfig}
|
|
170
176
|
{...buildCallbacks(modalState.webViewConfig)}
|
|
171
177
|
/>
|
|
172
178
|
)}
|
|
173
179
|
{modalState.mode === 'inline' && modalState.inlineConfig && (
|
|
174
180
|
<FincraInlineCheckout
|
|
181
|
+
key={sessionId}
|
|
175
182
|
{...modalState.inlineConfig}
|
|
176
183
|
{...buildCallbacks(modalState.inlineConfig)}
|
|
177
184
|
/>
|
|
@@ -230,7 +237,6 @@ export function _unregisterHostRef(): void {
|
|
|
230
237
|
* currency: 'NGN',
|
|
231
238
|
* customerEmail: 'user@example.com',
|
|
232
239
|
* customerName: 'Jane Doe',
|
|
233
|
-
* customerPhoneNumber: '08012345678',
|
|
234
240
|
* feeBearer: 'customer',
|
|
235
241
|
* });
|
|
236
242
|
*
|