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,165 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
// ─── Primitives ───────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
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
|
+
export const 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
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export type FincraCurrency =
|
|
24
|
+
| 'NGN'
|
|
25
|
+
| 'USD'
|
|
26
|
+
| 'GBP'
|
|
27
|
+
| 'EUR'
|
|
28
|
+
| 'GHS'
|
|
29
|
+
| 'KES'
|
|
30
|
+
| 'ZAR'
|
|
31
|
+
| 'UGX'
|
|
32
|
+
| 'XAF'
|
|
33
|
+
| 'XOF';
|
|
34
|
+
|
|
35
|
+
export const FeeBearer = {
|
|
36
|
+
Business: 'business',
|
|
37
|
+
Customer: 'customer',
|
|
38
|
+
business: 'business',
|
|
39
|
+
customer: 'customer',
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
export type FeeBearer = 'business' | 'customer';
|
|
43
|
+
|
|
44
|
+
// ─── Payment Data Models ───────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/** Normalized successful payment response. */
|
|
47
|
+
export interface FincraPaymentResponse {
|
|
48
|
+
/** Merchant/customer reference — mirrors Flutter's `FincraPaymentResponse.reference`. */
|
|
49
|
+
reference: string;
|
|
50
|
+
/** Fincra internal transaction ID. */
|
|
51
|
+
transactionId: string;
|
|
52
|
+
/** Payment status string (e.g., 'success', 'failed'). */
|
|
53
|
+
status: string;
|
|
54
|
+
/** Optional human-readable message from Fincra. */
|
|
55
|
+
message?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Full raw response map. All values are strings.
|
|
58
|
+
* For inline mode, numeric/boolean fields from Fincra (e.g. `amount`) are
|
|
59
|
+
* coerced to strings to match the URL-params format used in WebView mode.
|
|
60
|
+
*/
|
|
61
|
+
rawResponse: Record<string, string>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Structured payment error. */
|
|
65
|
+
export interface FincraPaymentError {
|
|
66
|
+
/** Error code string (e.g., 'timeout', 'cancelled', HTTP status code). */
|
|
67
|
+
code: string;
|
|
68
|
+
/** Human-readable error description. */
|
|
69
|
+
message: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Result Discriminated Union ────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* All possible outcomes of a Fincra Checkout session.
|
|
76
|
+
* Use `result.type` to discriminate:
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* switch (result.type) {
|
|
81
|
+
* case 'success': handleSuccess(result.response); break;
|
|
82
|
+
* case 'error': handleError(result.error); break;
|
|
83
|
+
* case 'cancelled': handleCancelled(); break;
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export type FincraCheckoutResult =
|
|
88
|
+
| { type: 'success'; response: FincraPaymentResponse }
|
|
89
|
+
| { type: 'error'; error: FincraPaymentError }
|
|
90
|
+
| { type: 'cancelled' };
|
|
91
|
+
|
|
92
|
+
// ─── Component Props ───────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/** Shared props across both checkout modes. */
|
|
95
|
+
export interface BaseCheckoutProps {
|
|
96
|
+
/** Called when payment is successfully completed. */
|
|
97
|
+
onSuccess?: (response: FincraPaymentResponse) => void;
|
|
98
|
+
/** Called when the payment fails or encounters an error. */
|
|
99
|
+
onFailed?: (error: FincraPaymentError) => void;
|
|
100
|
+
/** Called when the user cancels/closes the checkout. */
|
|
101
|
+
onCancelled?: () => void;
|
|
102
|
+
/** Title shown in the navigation header. Defaults to 'Secure Checkout'. */
|
|
103
|
+
headerTitle?: string;
|
|
104
|
+
/** Background color of the navigation header bar. */
|
|
105
|
+
headerBackgroundColor?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Text/icon color inside the navigation header.
|
|
108
|
+
* Also determines the status bar style:
|
|
109
|
+
* - `'#000000'` (default) → `dark-content` status bar icons
|
|
110
|
+
* - Any other value → `light-content` status bar icons
|
|
111
|
+
*/
|
|
112
|
+
headerTintColor?: string;
|
|
113
|
+
/** If true, shows a confirmation dialog before dismissing. Default: false. */
|
|
114
|
+
showCancelConfirmationDialog?: boolean;
|
|
115
|
+
/** Custom loading indicator to display while the WebView is loading. */
|
|
116
|
+
loadingComponent?: ReactNode;
|
|
117
|
+
/** Custom close icon/element for the header. */
|
|
118
|
+
closeIcon?: ReactNode;
|
|
119
|
+
/** Custom error screen renderer for network/loading recovery. */
|
|
120
|
+
renderError?: (
|
|
121
|
+
error: { code: string; message: string },
|
|
122
|
+
retry: () => void
|
|
123
|
+
) => ReactNode;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Configuration for the WebView-based checkout (backend-generated URL). */
|
|
127
|
+
export interface WebViewCheckoutConfig extends BaseCheckoutProps {
|
|
128
|
+
/**
|
|
129
|
+
* The backend-generated Fincra checkout URL.
|
|
130
|
+
* Generate this server-side using the Fincra API with your **secret key**.
|
|
131
|
+
* Never generate this on the client.
|
|
132
|
+
*/
|
|
133
|
+
checkoutUrl: string;
|
|
134
|
+
/**
|
|
135
|
+
* Your backend redirect URL. When Fincra navigates to this URL,
|
|
136
|
+
* the SDK intercepts and resolves the payment result.
|
|
137
|
+
* If omitted, the SDK falls back to detecting `status` + `reference` query params.
|
|
138
|
+
*/
|
|
139
|
+
redirectUrl?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Configuration for the Inline JavaScript checkout (frontend-initiated). */
|
|
143
|
+
export interface InlineCheckoutConfig extends BaseCheckoutProps {
|
|
144
|
+
/**
|
|
145
|
+
* Your Fincra **public key** (starts with `pk_`).
|
|
146
|
+
* @security Never use your secret key in mobile app code.
|
|
147
|
+
*/
|
|
148
|
+
publicKey: string;
|
|
149
|
+
/** Amount to charge in the smallest currency unit (e.g., kobo for NGN). */
|
|
150
|
+
amount: number;
|
|
151
|
+
/** Currency for the transaction. */
|
|
152
|
+
currency: FincraCurrency;
|
|
153
|
+
/** Customer's email address. */
|
|
154
|
+
customerEmail: string;
|
|
155
|
+
/** Customer's full name. */
|
|
156
|
+
customerName: string;
|
|
157
|
+
/** Customer's phone number. */
|
|
158
|
+
customerPhoneNumber: string;
|
|
159
|
+
/** Who bears the Fincra processing fee. */
|
|
160
|
+
feeBearer: FeeBearer;
|
|
161
|
+
/** Optional unique transaction reference. Fincra generates one if omitted. */
|
|
162
|
+
reference?: string;
|
|
163
|
+
/** Restrict checkout to specific payment methods (e.g., ['card', 'bank_transfer']). */
|
|
164
|
+
paymentMethods?: string[];
|
|
165
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { FincraPaymentResponse } from '../types';
|
|
2
|
+
|
|
3
|
+
// ─── URL Handler ──────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Direct TypeScript port of flutter_fincra_checkout/lib/src/utils/url_handler.dart
|
|
6
|
+
// Mirrors UrlHandler.isCompletionUrl() and UrlHandler.extractResponseParams()
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Utilities for detecting Fincra payment completion URLs and
|
|
10
|
+
* extracting normalized response parameters.
|
|
11
|
+
*/
|
|
12
|
+
export 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: string, expectedRedirectUrl?: string): boolean {
|
|
24
|
+
if (!url) return false;
|
|
25
|
+
|
|
26
|
+
if (expectedRedirectUrl && expectedRedirectUrl.length > 0) {
|
|
27
|
+
return url.startsWith(expectedRedirectUrl);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Fallback: detect via query parameters
|
|
31
|
+
try {
|
|
32
|
+
const params = UrlHandler._parseQueryParams(url);
|
|
33
|
+
const hasStatus =
|
|
34
|
+
params.has('status') || params.has('payment_status');
|
|
35
|
+
const hasReference = params.has('reference');
|
|
36
|
+
return hasStatus && hasReference;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extracts all query parameters from the URL as a `Record<string, string>`.
|
|
44
|
+
*
|
|
45
|
+
* @param url - The completion URL from Fincra.
|
|
46
|
+
*/
|
|
47
|
+
static extractResponseParams(url: string): Record<string, string> {
|
|
48
|
+
try {
|
|
49
|
+
const params = UrlHandler._parseQueryParams(url);
|
|
50
|
+
const result: Record<string, string> = {};
|
|
51
|
+
params.forEach((value, key) => {
|
|
52
|
+
result[key] = value;
|
|
53
|
+
});
|
|
54
|
+
return result;
|
|
55
|
+
} catch {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Builds a normalized `FincraPaymentResponse` from URL query parameters.
|
|
62
|
+
*
|
|
63
|
+
* Mirrors `FincraPaymentResponse.fromUrlParams()` in Flutter, including
|
|
64
|
+
* the reference normalization logic (customerReference → merchantReference → reference).
|
|
65
|
+
*
|
|
66
|
+
* @param params - Raw query params extracted from the completion URL.
|
|
67
|
+
*/
|
|
68
|
+
static parsePaymentResponse(
|
|
69
|
+
params: Record<string, string>
|
|
70
|
+
): FincraPaymentResponse {
|
|
71
|
+
// Fincra sometimes returns the merchant ref under different keys
|
|
72
|
+
const customRef =
|
|
73
|
+
params['customerReference'] ?? params['merchantReference'];
|
|
74
|
+
const internalRef =
|
|
75
|
+
params['transactionReference'] ?? params['transactionId'];
|
|
76
|
+
|
|
77
|
+
const finalRef = customRef ?? params['reference'] ?? '';
|
|
78
|
+
const finalTxId =
|
|
79
|
+
internalRef ?? (customRef != null ? params['reference'] ?? '' : '');
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
reference: finalRef,
|
|
83
|
+
transactionId: finalTxId ?? '',
|
|
84
|
+
status: params['status'] ?? 'unknown',
|
|
85
|
+
message: params['message'],
|
|
86
|
+
rawResponse: params,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Determines if a status string represents a successful payment.
|
|
92
|
+
*
|
|
93
|
+
* @param status - The raw status string from Fincra.
|
|
94
|
+
*/
|
|
95
|
+
static isSuccessStatus(status: string): boolean {
|
|
96
|
+
const normalized = status.toLowerCase().trim();
|
|
97
|
+
return normalized === 'success' || normalized === 'successful';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Internal ────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Parses URL query string into a `URLSearchParams`-like `Map`.
|
|
104
|
+
* Works in React Native (no DOM `URL` API available).
|
|
105
|
+
*/
|
|
106
|
+
private static _parseQueryParams(url: string): Map<string, string> {
|
|
107
|
+
const map = new Map<string, string>();
|
|
108
|
+
const queryStart = url.indexOf('?');
|
|
109
|
+
if (queryStart === -1) return map;
|
|
110
|
+
|
|
111
|
+
const queryString = url.slice(queryStart + 1);
|
|
112
|
+
const pairs = queryString.split('&');
|
|
113
|
+
|
|
114
|
+
for (const pair of pairs) {
|
|
115
|
+
const eqIdx = pair.indexOf('=');
|
|
116
|
+
if (eqIdx === -1) continue;
|
|
117
|
+
const key = decodeURIComponent(pair.slice(0, eqIdx).replace(/\+/g, ' '));
|
|
118
|
+
const val = decodeURIComponent(pair.slice(eqIdx + 1).replace(/\+/g, ' '));
|
|
119
|
+
if (key) map.set(key, val);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return map;
|
|
123
|
+
}
|
|
124
|
+
}
|