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.
@@ -0,0 +1,300 @@
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.FincraWebViewCheckout = FincraWebViewCheckout;
37
+ const react_1 = __importStar(require("react"));
38
+ const react_native_1 = require("react-native");
39
+ const react_native_safe_area_context_1 = require("react-native-safe-area-context");
40
+ const react_native_webview_1 = require("react-native-webview");
41
+ const UrlHandler_1 = require("../utils/UrlHandler");
42
+ /**
43
+ * A full-screen WebView that loads a backend-generated Fincra checkout URL.
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * <FincraWebViewCheckout
48
+ * checkoutUrl="https://checkout.fincra.com/pay/..."
49
+ * redirectUrl="https://your-backend.com/payment/callback"
50
+ * onSuccess={(res) => console.log('Paid:', res.reference)}
51
+ * onFailed={(err) => console.log('Failed:', err.message)}
52
+ * onCancelled={() => navigation.goBack()}
53
+ * />
54
+ * ```
55
+ */
56
+ function FincraWebViewCheckout({ checkoutUrl, redirectUrl, headerTitle = 'Secure Checkout', headerBackgroundColor = '#FFFFFF', headerTintColor = '#000000', showCancelConfirmationDialog = false, loadingComponent, closeIcon, renderError, onSuccess, onFailed, onCancelled, }) {
57
+ const [isLoading, setIsLoading] = (0, react_1.useState)(true);
58
+ const [errorState, setErrorState] = (0, react_1.useState)(null);
59
+ const [reloadKey, setReloadKey] = (0, react_1.useState)(0);
60
+ const webViewRef = (0, react_1.useRef)(null);
61
+ const hasCompleted = (0, react_1.useRef)(false);
62
+ // Fix #10: Stable ref for handleCancellation — the BackHandler effect
63
+ // always calls through this ref, never capturing a stale closure.
64
+ const handleCancellationRef = (0, react_1.useRef)(() => { });
65
+ // ── Back handler (Android) ──────────────────────────────────────────────────
66
+ (0, react_1.useEffect)(() => {
67
+ if (react_native_1.Platform.OS !== 'android')
68
+ return;
69
+ const subscription = react_native_1.BackHandler.addEventListener('hardwareBackPress', () => {
70
+ handleCancellationRef.current();
71
+ return true; // consume the event
72
+ });
73
+ return () => subscription.remove();
74
+ }, []); // safe: always calls through the ref
75
+ // ── Completion handler ──────────────────────────────────────────────────────
76
+ const handleCompletion = (0, react_1.useCallback)((url) => {
77
+ if (hasCompleted.current)
78
+ return;
79
+ hasCompleted.current = true;
80
+ const params = UrlHandler_1.UrlHandler.extractResponseParams(url);
81
+ // Safely assume success if status is missing (matches Flutter behaviour)
82
+ const rawStatus = params['status']?.toLowerCase() ?? 'success';
83
+ if (UrlHandler_1.UrlHandler.isSuccessStatus(rawStatus)) {
84
+ const response = UrlHandler_1.UrlHandler.parsePaymentResponse(params);
85
+ onSuccess?.(response);
86
+ }
87
+ else {
88
+ const err = {
89
+ code: rawStatus,
90
+ message: params['message'] ?? 'Payment failed',
91
+ };
92
+ onFailed?.(err);
93
+ }
94
+ }, [onSuccess, onFailed]);
95
+ // ── URL interception ────────────────────────────────────────────────────────
96
+ const onShouldStartLoadWithRequest = (0, react_1.useCallback)((request) => {
97
+ if (UrlHandler_1.UrlHandler.isCompletionUrl(request.url, redirectUrl)) {
98
+ handleCompletion(request.url);
99
+ return false; // prevent navigation — mirrors NavigationDecision.prevent
100
+ }
101
+ return true;
102
+ }, [redirectUrl, handleCompletion]);
103
+ // ── Cancellation ────────────────────────────────────────────────────────────
104
+ const handleCancellation = (0, react_1.useCallback)(() => {
105
+ if (hasCompleted.current)
106
+ return;
107
+ if (showCancelConfirmationDialog) {
108
+ react_native_1.Alert.alert('Cancel Payment?', 'Are you sure you want to cancel this payment?', [
109
+ { text: 'No', style: 'cancel' },
110
+ {
111
+ text: 'Yes',
112
+ style: 'destructive',
113
+ onPress: () => {
114
+ if (!hasCompleted.current) {
115
+ hasCompleted.current = true;
116
+ onCancelled?.();
117
+ }
118
+ },
119
+ },
120
+ ]);
121
+ }
122
+ else {
123
+ hasCompleted.current = true;
124
+ onCancelled?.();
125
+ }
126
+ }, [showCancelConfirmationDialog, onCancelled]);
127
+ // Keep the ref in sync after every render (Fix #10)
128
+ (0, react_1.useEffect)(() => {
129
+ handleCancellationRef.current = handleCancellation;
130
+ });
131
+ // ── WebView error ───────────────────────────────────────────────────────────
132
+ const handleError = (0, react_1.useCallback)((syntheticEvent) => {
133
+ if (hasCompleted.current)
134
+ return;
135
+ const { nativeEvent } = syntheticEvent;
136
+ const err = {
137
+ code: String(nativeEvent.code ?? 'webview_error'),
138
+ message: nativeEvent.description ?? 'A WebView error occurred.',
139
+ };
140
+ setErrorState(err);
141
+ setIsLoading(false);
142
+ }, []);
143
+ // ── HTTP error (non-completion URLs only) ───────────────────────────────────
144
+ const handleHttpError = (0, react_1.useCallback)((e) => {
145
+ if (!UrlHandler_1.UrlHandler.isCompletionUrl(e.nativeEvent.url, redirectUrl)) {
146
+ handleError(e);
147
+ }
148
+ }, [redirectUrl, handleError]);
149
+ // ── Retry handler ───────────────────────────────────────────────────────────
150
+ const handleRetry = (0, react_1.useCallback)(() => {
151
+ setErrorState(null);
152
+ setIsLoading(true);
153
+ setReloadKey((key) => key + 1);
154
+ }, []);
155
+ // ── Navigation state change (iOS fallback) ──────────────────────────────────
156
+ // Fix #2: guard setIsLoading — don't flip loading state after completion.
157
+ const onNavigationStateChange = (0, react_1.useCallback)((navState) => {
158
+ if (hasCompleted.current)
159
+ return;
160
+ if (navState.url &&
161
+ UrlHandler_1.UrlHandler.isCompletionUrl(navState.url, redirectUrl)) {
162
+ handleCompletion(navState.url);
163
+ }
164
+ }, [redirectUrl, handleCompletion]);
165
+ // ── Computed status bar style (Fix #11) ─────────────────────────────────────
166
+ const statusBarStyle = headerTintColor === '#000000' ? 'dark-content' : 'light-content';
167
+ // ── Render ──────────────────────────────────────────────────────────────────
168
+ return (react_1.default.createElement(react_native_safe_area_context_1.SafeAreaView, { style: styles.container },
169
+ react_1.default.createElement(react_native_1.StatusBar, { barStyle: statusBarStyle, backgroundColor: headerBackgroundColor }),
170
+ react_1.default.createElement(react_native_1.View, { style: [styles.header, { backgroundColor: headerBackgroundColor }] },
171
+ react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.closeButton, onPress: handleCancellation, accessibilityLabel: "Close checkout", accessibilityRole: "button", hitSlop: { top: 10, bottom: 10, left: 10, right: 10 } }, closeIcon ?? (react_1.default.createElement(react_native_1.Text, { style: [styles.closeIcon, { color: headerTintColor }] }, "\u2715"))),
172
+ react_1.default.createElement(react_native_1.Text, { style: [styles.headerTitle, { color: headerTintColor }], numberOfLines: 1 }, headerTitle),
173
+ react_1.default.createElement(react_native_1.View, { style: styles.closeButton })),
174
+ react_1.default.createElement(react_native_1.View, { style: styles.webViewContainer },
175
+ react_1.default.createElement(react_native_webview_1.WebView, { key: reloadKey, ref: webViewRef, source: { uri: checkoutUrl }, style: styles.webView, javaScriptEnabled: true, domStorageEnabled: true, startInLoadingState: false, onLoadStart: () => {
176
+ if (!hasCompleted.current)
177
+ setIsLoading(true);
178
+ }, onLoadEnd: () => {
179
+ if (!hasCompleted.current)
180
+ setIsLoading(false);
181
+ }, onError: handleError, onHttpError: handleHttpError, onShouldStartLoadWithRequest: onShouldStartLoadWithRequest, onNavigationStateChange: onNavigationStateChange,
182
+ // Fix #14: restrict to HTTPS + about:blank — prevents intent:// and
183
+ // other dangerous scheme navigations in a payment context.
184
+ originWhitelist: ['https://*', 'about:blank'] }),
185
+ isLoading && !errorState && (react_1.default.createElement(react_native_1.View, { style: styles.loadingOverlay, pointerEvents: "none" }, loadingComponent ?? (react_1.default.createElement(react_native_1.ActivityIndicator, { size: "large", color: "#0066FF" })))),
186
+ errorState && (react_1.default.createElement(react_native_1.View, { style: styles.errorOverlay }, renderError ? (renderError(errorState, handleRetry)) : (react_1.default.createElement(react_native_1.View, { style: styles.errorContainer },
187
+ react_1.default.createElement(react_native_1.Text, { style: styles.errorIcon }, "\u26A0\uFE0F"),
188
+ react_1.default.createElement(react_native_1.Text, { style: styles.errorTitle }, "Connection Error"),
189
+ react_1.default.createElement(react_native_1.Text, { style: styles.errorMessage }, errorState.message),
190
+ react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.retryButton, onPress: handleRetry, accessibilityRole: "button", accessibilityLabel: "Retry loading checkout" },
191
+ react_1.default.createElement(react_native_1.Text, { style: styles.retryButtonText }, "Retry")),
192
+ react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.cancelButton, onPress: handleCancellation, accessibilityRole: "button", accessibilityLabel: "Cancel checkout" },
193
+ react_1.default.createElement(react_native_1.Text, { style: styles.cancelButtonText }, "Cancel")))))))));
194
+ }
195
+ // ─── Styles ────────────────────────────────────────────────────────────────────
196
+ const styles = react_native_1.StyleSheet.create({
197
+ container: {
198
+ flex: 1,
199
+ backgroundColor: '#FFFFFF',
200
+ },
201
+ header: {
202
+ flexDirection: 'row',
203
+ alignItems: 'center',
204
+ justifyContent: 'space-between',
205
+ height: 56,
206
+ paddingHorizontal: 12,
207
+ borderBottomWidth: react_native_1.StyleSheet.hairlineWidth,
208
+ borderBottomColor: '#E5E7EB',
209
+ elevation: 2,
210
+ shadowColor: '#000',
211
+ shadowOffset: { width: 0, height: 1 },
212
+ shadowOpacity: 0.08,
213
+ shadowRadius: 2,
214
+ },
215
+ closeButton: {
216
+ width: 40,
217
+ height: 40,
218
+ alignItems: 'center',
219
+ justifyContent: 'center',
220
+ },
221
+ closeIcon: {
222
+ fontSize: 18,
223
+ fontWeight: '600',
224
+ },
225
+ headerTitle: {
226
+ flex: 1,
227
+ textAlign: 'center',
228
+ fontSize: 16,
229
+ fontWeight: '600',
230
+ letterSpacing: 0.2,
231
+ },
232
+ webViewContainer: {
233
+ flex: 1,
234
+ },
235
+ webView: {
236
+ flex: 1,
237
+ backgroundColor: 'transparent',
238
+ },
239
+ loadingOverlay: {
240
+ ...react_native_1.StyleSheet.absoluteFillObject,
241
+ alignItems: 'center',
242
+ justifyContent: 'center',
243
+ backgroundColor: 'rgba(255,255,255,0.85)',
244
+ },
245
+ errorOverlay: {
246
+ ...react_native_1.StyleSheet.absoluteFillObject,
247
+ backgroundColor: '#FFFFFF',
248
+ alignItems: 'center',
249
+ justifyContent: 'center',
250
+ padding: 24,
251
+ },
252
+ errorContainer: {
253
+ alignItems: 'center',
254
+ maxWidth: 320,
255
+ width: '100%',
256
+ },
257
+ errorIcon: {
258
+ fontSize: 48,
259
+ marginBottom: 16,
260
+ },
261
+ errorTitle: {
262
+ fontSize: 20,
263
+ fontWeight: '700',
264
+ color: '#1F2937',
265
+ marginBottom: 8,
266
+ textAlign: 'center',
267
+ },
268
+ errorMessage: {
269
+ fontSize: 14,
270
+ color: '#6B7280',
271
+ textAlign: 'center',
272
+ marginBottom: 24,
273
+ lineHeight: 20,
274
+ },
275
+ retryButton: {
276
+ backgroundColor: '#0066FF',
277
+ paddingVertical: 12,
278
+ paddingHorizontal: 32,
279
+ borderRadius: 8,
280
+ width: '100%',
281
+ alignItems: 'center',
282
+ marginBottom: 12,
283
+ },
284
+ retryButtonText: {
285
+ color: '#FFFFFF',
286
+ fontSize: 16,
287
+ fontWeight: '600',
288
+ },
289
+ cancelButton: {
290
+ paddingVertical: 12,
291
+ paddingHorizontal: 32,
292
+ width: '100%',
293
+ alignItems: 'center',
294
+ },
295
+ cancelButtonText: {
296
+ color: '#6B7280',
297
+ fontSize: 15,
298
+ fontWeight: '500',
299
+ },
300
+ });
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ // ─── Public API ───────────────────────────────────────────────────────────────
3
+ //
4
+ // This is the single entry point for react-native-fincra-checkout.
5
+ // Import only what you need:
6
+ //
7
+ // import { FincraCheckout, FincraCheckoutHost } from 'react-native-fincra-checkout';
8
+ // import type { FincraCheckoutResult, WebViewCheckoutConfig } from 'react-native-fincra-checkout';
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.FeeBearer = exports.FincraCurrency = exports.FincraInlineCheckout = exports.FincraWebViewCheckout = exports.FincraCheckoutHost = exports.FincraCheckout = void 0;
11
+ // ── Imperative API & Host ──────────────────────────────────────────────────────
12
+ var FincraCheckout_1 = require("./checkout/FincraCheckout");
13
+ Object.defineProperty(exports, "FincraCheckout", { enumerable: true, get: function () { return FincraCheckout_1.FincraCheckout; } });
14
+ Object.defineProperty(exports, "FincraCheckoutHost", { enumerable: true, get: function () { return FincraCheckout_1.FincraCheckoutHostRegistrar; } });
15
+ // ── Declarative Components ─────────────────────────────────────────────────────
16
+ var FincraWebViewCheckout_1 = require("./components/FincraWebViewCheckout");
17
+ Object.defineProperty(exports, "FincraWebViewCheckout", { enumerable: true, get: function () { return FincraWebViewCheckout_1.FincraWebViewCheckout; } });
18
+ var FincraInlineCheckout_1 = require("./components/FincraInlineCheckout");
19
+ Object.defineProperty(exports, "FincraInlineCheckout", { enumerable: true, get: function () { return FincraInlineCheckout_1.FincraInlineCheckout; } });
20
+ // ── Primitives & Types ─────────────────────────────────────────────────────────
21
+ var types_1 = require("./types");
22
+ Object.defineProperty(exports, "FincraCurrency", { enumerable: true, get: function () { return types_1.FincraCurrency; } });
23
+ Object.defineProperty(exports, "FeeBearer", { enumerable: true, get: function () { return types_1.FeeBearer; } });
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FincraBridgeEvent = void 0;
4
+ exports.parseMessage = parseMessage;
5
+ const UrlHandler_1 = require("../utils/UrlHandler");
6
+ // ─── JS Bridge ────────────────────────────────────────────────────────────────
7
+ //
8
+ // Direct TypeScript port of flutter_fincra_checkout/lib/src/inline/javascript_bridge.dart
9
+ // Handles parsing of postMessage events from the Fincra inline JS SDK.
10
+ /** Events that can be posted from the Fincra inline JavaScript SDK. */
11
+ var FincraBridgeEvent;
12
+ (function (FincraBridgeEvent) {
13
+ /** The Fincra SDK has loaded and is ready — hide the loader. */
14
+ FincraBridgeEvent["Ready"] = "ready";
15
+ /** Payment completed successfully. */
16
+ FincraBridgeEvent["Success"] = "success";
17
+ /** The user closed the Fincra checkout modal. */
18
+ FincraBridgeEvent["Closed"] = "closed";
19
+ /** The Fincra SDK emitted an error (e.g., load failure). */
20
+ FincraBridgeEvent["Error"] = "error";
21
+ /** Unrecognized or malformed event — should be ignored. */
22
+ FincraBridgeEvent["Unknown"] = "unknown";
23
+ })(FincraBridgeEvent || (exports.FincraBridgeEvent = FincraBridgeEvent = {}));
24
+ /**
25
+ * Parses a raw `postMessage` JSON string into a `FincraBridgeMessage`.
26
+ *
27
+ * Expected format from the HTML template:
28
+ * ```json
29
+ * { "event": "success", "data": { "reference": "...", ... } }
30
+ * ```
31
+ *
32
+ * On any parse failure, returns an `Unknown` event (mirrors Flutter's catch block).
33
+ */
34
+ function parseMessage(jsonString) {
35
+ try {
36
+ const map = JSON.parse(jsonString);
37
+ // Guard: JSON.parse('null') returns null, not an object
38
+ if (map == null || typeof map !== 'object') {
39
+ return { event: FincraBridgeEvent.Unknown };
40
+ }
41
+ const eventStr = typeof map['event'] === 'string' ? map['event'] : null;
42
+ const event = _parseEvent(eventStr);
43
+ if (event === FincraBridgeEvent.Success &&
44
+ map['data'] != null &&
45
+ typeof map['data'] === 'object' &&
46
+ !Array.isArray(map['data'])) {
47
+ // Fix #5: use a spread copy instead of mutating the JSON.parse result
48
+ const rawData = map['data'];
49
+ const dataMap = { ...rawData };
50
+ // Ensure status is always set for the response
51
+ if (!dataMap['status']) {
52
+ dataMap['status'] = 'success';
53
+ }
54
+ // Coerce all values to strings (mirrors Flutter's `.map((k,v) => MapEntry(k, v.toString()))`)
55
+ const params = {};
56
+ for (const [k, v] of Object.entries(dataMap)) {
57
+ params[k] = String(v);
58
+ }
59
+ return { event, data: UrlHandler_1.UrlHandler.parsePaymentResponse(params) };
60
+ }
61
+ if (event === FincraBridgeEvent.Error &&
62
+ map['data'] != null &&
63
+ typeof map['data'] === 'object' &&
64
+ !Array.isArray(map['data'])) {
65
+ const dataMap = map['data'];
66
+ const errorMessage = typeof dataMap['message'] === 'string'
67
+ ? dataMap['message']
68
+ : 'An unknown error occurred';
69
+ return { event, data: { message: errorMessage } };
70
+ }
71
+ return { event };
72
+ }
73
+ catch {
74
+ return { event: FincraBridgeEvent.Unknown };
75
+ }
76
+ }
77
+ // ── Internal ──────────────────────────────────────────────────────────────────
78
+ function _parseEvent(eventStr) {
79
+ switch (eventStr) {
80
+ case 'ready':
81
+ return FincraBridgeEvent.Ready;
82
+ case 'success':
83
+ return FincraBridgeEvent.Success;
84
+ case 'closed':
85
+ return FincraBridgeEvent.Closed;
86
+ case 'error':
87
+ return FincraBridgeEvent.Error;
88
+ default:
89
+ return FincraBridgeEvent.Unknown;
90
+ }
91
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateInlineHtml = generateInlineHtml;
4
+ // ─── HTML Generator ────────────────────────────────────────────────────────────
5
+ //
6
+ // Mirrors `_generateHtml()` in flutter_fincra_checkout/lib/src/inline/inline_checkout.dart
7
+ //
8
+ // Security: ALL user-supplied string values are encoded via JSON.stringify(),
9
+ // which escapes special characters and wraps in double-quotes, preventing
10
+ // HTML/JS injection attacks (same approach as the Flutter implementation).
11
+ const FINCRA_CDN_URL = 'https://unpkg.com/@fincra-engineering/checkout@2.2.0/dist/inline.min.js';
12
+ /**
13
+ * Generates the self-contained HTML page that loads the Fincra inline JS SDK,
14
+ * initializes it with the provided config, and posts lifecycle events back to
15
+ * the React Native app via `window.ReactNativeWebView.postMessage(...)`.
16
+ *
17
+ * This function is **pure** — given the same config it always returns the same
18
+ * string, making it safe to memoize with `useMemo`.
19
+ *
20
+ * @param config - The inline payment configuration (payment fields only).
21
+ * @returns A complete HTML string to be loaded into a WebView.
22
+ */
23
+ function generateInlineHtml(config) {
24
+ // ── Safe encoding — all string values JSON-encoded to prevent injection ──
25
+ const key = JSON.stringify(config.publicKey);
26
+ const amount = config.amount; // numeric — safe to embed directly
27
+ const currency = JSON.stringify(config.currency.toUpperCase());
28
+ const name = JSON.stringify(config.customerName);
29
+ const email = JSON.stringify(config.customerEmail);
30
+ const phone = JSON.stringify(config.customerPhoneNumber);
31
+ const feeBearer = JSON.stringify(config.feeBearer);
32
+ // Optional fields — only emit the JS property if the value is present
33
+ const referenceLine = config.reference != null
34
+ ? `reference: ${JSON.stringify(config.reference)},`
35
+ : '';
36
+ const paymentMethodsLine = config.paymentMethods != null && config.paymentMethods.length > 0
37
+ ? `paymentMethods: ${JSON.stringify(config.paymentMethods)},`
38
+ : '';
39
+ return `<!DOCTYPE html>
40
+ <html>
41
+ <head>
42
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
43
+ <script src="${FINCRA_CDN_URL}"></script>
44
+ <style>
45
+ html, body {
46
+ margin: 0;
47
+ padding: 0;
48
+ background-color: transparent;
49
+ -webkit-overflow-scrolling: touch;
50
+ }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <script>
55
+ /**
56
+ * Posts a structured message to the React Native host.
57
+ * Uses window.ReactNativeWebView which is injected by react-native-webview.
58
+ */
59
+ function postToRN(event, data) {
60
+ if (window.ReactNativeWebView) {
61
+ window.ReactNativeWebView.postMessage(JSON.stringify({ event: event, data: data || null }));
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Retry loop — waits up to 15 seconds (150 × 100ms) for window.Fincra to load.
67
+ * Mirrors the identical pattern in the Flutter package's _generateHtml().
68
+ */
69
+ function initFincra(attempts) {
70
+ if (attempts === undefined) attempts = 0;
71
+
72
+ if (typeof Fincra === 'undefined') {
73
+ if (attempts > 150) {
74
+ postToRN('error', { message: 'Fincra SDK failed to load. Check your internet connection.' });
75
+ return;
76
+ }
77
+ setTimeout(function() { initFincra(attempts + 1); }, 100);
78
+ return;
79
+ }
80
+
81
+ // SDK is available — signal "ready" so the host hides the loading spinner
82
+ postToRN('ready', null);
83
+
84
+ var options = {
85
+ key: ${key},
86
+ amount: ${amount},
87
+ currency: ${currency},
88
+ ${referenceLine}
89
+ ${paymentMethodsLine}
90
+ feeBearer: ${feeBearer},
91
+ customer: {
92
+ name: ${name},
93
+ email: ${email},
94
+ phoneNumber: ${phone},
95
+ },
96
+ onClose: function() {
97
+ postToRN('closed', null);
98
+ },
99
+ onSuccess: function(data) {
100
+ postToRN('success', data);
101
+ },
102
+ };
103
+
104
+ Fincra.initialize(options);
105
+ }
106
+
107
+ window.onload = function() { initFincra(0); };
108
+ </script>
109
+ </body>
110
+ </html>`;
111
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FeeBearer = exports.FincraCurrency = void 0;
4
+ // ─── Primitives ───────────────────────────────────────────────────────────────
5
+ /**
6
+ * Currencies supported by the Fincra platform.
7
+ * Expands the Flutter enum (ngn, kes, ugx, ghs, zar, xaf, xof) with
8
+ * additional global currencies from the prompt spec.
9
+ */
10
+ exports.FincraCurrency = {
11
+ NGN: 'NGN',
12
+ USD: 'USD',
13
+ GBP: 'GBP',
14
+ EUR: 'EUR',
15
+ GHS: 'GHS',
16
+ KES: 'KES',
17
+ ZAR: 'ZAR',
18
+ UGX: 'UGX',
19
+ XAF: 'XAF',
20
+ XOF: 'XOF',
21
+ };
22
+ exports.FeeBearer = {
23
+ Business: 'business',
24
+ Customer: 'customer',
25
+ business: 'business',
26
+ customer: 'customer',
27
+ };
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UrlHandler = void 0;
4
+ // ─── URL Handler ──────────────────────────────────────────────────────────────
5
+ //
6
+ // Direct TypeScript port of flutter_fincra_checkout/lib/src/utils/url_handler.dart
7
+ // Mirrors UrlHandler.isCompletionUrl() and UrlHandler.extractResponseParams()
8
+ /**
9
+ * Utilities for detecting Fincra payment completion URLs and
10
+ * extracting normalized response parameters.
11
+ */
12
+ class UrlHandler {
13
+ /**
14
+ * Returns `true` if the given URL signals a Fincra payment completion.
15
+ *
16
+ * Logic (mirrors Flutter):
17
+ * 1. If `expectedRedirectUrl` is provided, check `url.startsWith(expectedRedirectUrl)`.
18
+ * 2. Fallback: Fincra appends `status` (or `payment_status`) AND `reference` as query params.
19
+ *
20
+ * @param url - The URL being navigated to.
21
+ * @param expectedRedirectUrl - The redirect URL you registered on your backend.
22
+ */
23
+ static isCompletionUrl(url, expectedRedirectUrl) {
24
+ if (!url)
25
+ return false;
26
+ if (expectedRedirectUrl && expectedRedirectUrl.length > 0) {
27
+ return url.startsWith(expectedRedirectUrl);
28
+ }
29
+ // Fallback: detect via query parameters
30
+ try {
31
+ const params = UrlHandler._parseQueryParams(url);
32
+ const hasStatus = params.has('status') || params.has('payment_status');
33
+ const hasReference = params.has('reference');
34
+ return hasStatus && hasReference;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ /**
41
+ * Extracts all query parameters from the URL as a `Record<string, string>`.
42
+ *
43
+ * @param url - The completion URL from Fincra.
44
+ */
45
+ static extractResponseParams(url) {
46
+ try {
47
+ const params = UrlHandler._parseQueryParams(url);
48
+ const result = {};
49
+ params.forEach((value, key) => {
50
+ result[key] = value;
51
+ });
52
+ return result;
53
+ }
54
+ catch {
55
+ return {};
56
+ }
57
+ }
58
+ /**
59
+ * Builds a normalized `FincraPaymentResponse` from URL query parameters.
60
+ *
61
+ * Mirrors `FincraPaymentResponse.fromUrlParams()` in Flutter, including
62
+ * the reference normalization logic (customerReference → merchantReference → reference).
63
+ *
64
+ * @param params - Raw query params extracted from the completion URL.
65
+ */
66
+ static parsePaymentResponse(params) {
67
+ // Fincra sometimes returns the merchant ref under different keys
68
+ const customRef = params['customerReference'] ?? params['merchantReference'];
69
+ const internalRef = params['transactionReference'] ?? params['transactionId'];
70
+ const finalRef = customRef ?? params['reference'] ?? '';
71
+ const finalTxId = internalRef ?? (customRef != null ? params['reference'] ?? '' : '');
72
+ return {
73
+ reference: finalRef,
74
+ transactionId: finalTxId ?? '',
75
+ status: params['status'] ?? 'unknown',
76
+ message: params['message'],
77
+ rawResponse: params,
78
+ };
79
+ }
80
+ /**
81
+ * Determines if a status string represents a successful payment.
82
+ *
83
+ * @param status - The raw status string from Fincra.
84
+ */
85
+ static isSuccessStatus(status) {
86
+ const normalized = status.toLowerCase().trim();
87
+ return normalized === 'success' || normalized === 'successful';
88
+ }
89
+ // ── Internal ────────────────────────────────────────────────────────────────
90
+ /**
91
+ * Parses URL query string into a `URLSearchParams`-like `Map`.
92
+ * Works in React Native (no DOM `URL` API available).
93
+ */
94
+ static _parseQueryParams(url) {
95
+ const map = new Map();
96
+ const queryStart = url.indexOf('?');
97
+ if (queryStart === -1)
98
+ return map;
99
+ const queryString = url.slice(queryStart + 1);
100
+ const pairs = queryString.split('&');
101
+ for (const pair of pairs) {
102
+ const eqIdx = pair.indexOf('=');
103
+ if (eqIdx === -1)
104
+ continue;
105
+ const key = decodeURIComponent(pair.slice(0, eqIdx).replace(/\+/g, ' '));
106
+ const val = decodeURIComponent(pair.slice(eqIdx + 1).replace(/\+/g, ' '));
107
+ if (key)
108
+ map.set(key, val);
109
+ }
110
+ return map;
111
+ }
112
+ }
113
+ exports.UrlHandler = UrlHandler;