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
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
TouchableOpacity,
|
|
11
11
|
View,
|
|
12
12
|
} from 'react-native';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
SafeAreaProvider,
|
|
15
|
+
SafeAreaView,
|
|
16
|
+
} from 'react-native-safe-area-context';
|
|
14
17
|
import { WebView } from 'react-native-webview';
|
|
15
18
|
import type {
|
|
16
19
|
WebViewMessageEvent,
|
|
@@ -48,7 +51,6 @@ const INIT_TIMEOUT_MS = 15_000;
|
|
|
48
51
|
* currency="NGN"
|
|
49
52
|
* customerEmail="user@example.com"
|
|
50
53
|
* customerName="John Doe"
|
|
51
|
-
* customerPhoneNumber="08012345678"
|
|
52
54
|
* feeBearer="customer"
|
|
53
55
|
* onSuccess={(res) => console.log(res.reference)}
|
|
54
56
|
* onCancelled={() => navigation.goBack()}
|
|
@@ -60,6 +62,7 @@ export function FincraInlineCheckout({
|
|
|
60
62
|
headerBackgroundColor = '#FFFFFF',
|
|
61
63
|
headerTintColor = '#000000',
|
|
62
64
|
showCancelConfirmationDialog = false,
|
|
65
|
+
showCloseButton = true,
|
|
63
66
|
loadingComponent,
|
|
64
67
|
closeIcon,
|
|
65
68
|
renderError,
|
|
@@ -73,9 +76,41 @@ export function FincraInlineCheckout({
|
|
|
73
76
|
const [errorState, setErrorState] = useState<FincraPaymentError | null>(null);
|
|
74
77
|
const [reloadKey, setReloadKey] = useState(0);
|
|
75
78
|
const webViewRef = useRef<WebView<object> | null>(null);
|
|
76
|
-
|
|
79
|
+
// `settledRef`: a result (success / error / cancel) has been delivered.
|
|
80
|
+
// `isMountedRef`: late bridge messages after unmount must be ignored.
|
|
81
|
+
const settledRef = useRef(false);
|
|
82
|
+
const isMountedRef = useRef(false);
|
|
77
83
|
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
78
84
|
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
isMountedRef.current = true;
|
|
87
|
+
return () => {
|
|
88
|
+
isMountedRef.current = false;
|
|
89
|
+
};
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
/** True while callbacks may still update state or deliver a result. */
|
|
93
|
+
const isActive = useCallback(
|
|
94
|
+
() => isMountedRef.current && !settledRef.current,
|
|
95
|
+
[]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const clearInitTimeout = useCallback(() => {
|
|
99
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
100
|
+
timeoutRef.current = null;
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
/** Single exit point — delivers at most one result per session. */
|
|
104
|
+
const settle = useCallback(
|
|
105
|
+
(deliver: () => void) => {
|
|
106
|
+
if (!isMountedRef.current || settledRef.current) return;
|
|
107
|
+
settledRef.current = true;
|
|
108
|
+
clearInitTimeout();
|
|
109
|
+
deliver();
|
|
110
|
+
},
|
|
111
|
+
[clearInitTimeout]
|
|
112
|
+
);
|
|
113
|
+
|
|
79
114
|
// Fix #3: Memoize HTML generation — prevents WebView reload on parent re-renders.
|
|
80
115
|
// The HTML is intentionally generated once per mount; payment params are immutable.
|
|
81
116
|
const configJson = JSON.stringify(paymentConfig);
|
|
@@ -84,24 +119,21 @@ export function FincraInlineCheckout({
|
|
|
84
119
|
[configJson]
|
|
85
120
|
);
|
|
86
121
|
|
|
87
|
-
// ── 15-second init timeout
|
|
122
|
+
// ── 15-second init timeout (re-armed on every Retry) ────────────────────────
|
|
88
123
|
useEffect(() => {
|
|
89
124
|
timeoutRef.current = setTimeout(() => {
|
|
90
|
-
if (!
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
125
|
+
if (!isActive()) return;
|
|
126
|
+
const err: FincraPaymentError = {
|
|
127
|
+
code: 'timeout',
|
|
128
|
+
message:
|
|
129
|
+
'Fincra Checkout failed to load. Please check your internet connection.',
|
|
130
|
+
};
|
|
131
|
+
setErrorState(err);
|
|
132
|
+
setIsLoading(false);
|
|
99
133
|
}, INIT_TIMEOUT_MS);
|
|
100
134
|
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
};
|
|
104
|
-
}, []);
|
|
135
|
+
return clearInitTimeout;
|
|
136
|
+
}, [reloadKey, isActive, clearInitTimeout]);
|
|
105
137
|
|
|
106
138
|
// Fix #10: Stable ref for handleCancellation so the BackHandler effect
|
|
107
139
|
// doesn't need it as a dependency and never captures a stale closure.
|
|
@@ -120,34 +152,35 @@ export function FincraInlineCheckout({
|
|
|
120
152
|
return () => subscription.remove();
|
|
121
153
|
}, []); // safe: always calls through the ref
|
|
122
154
|
|
|
123
|
-
// ──
|
|
155
|
+
// ── Load errors (main frame only — see FincraWebViewCheckout) ───────────────
|
|
156
|
+
const showLoadError = useCallback(
|
|
157
|
+
(err: FincraPaymentError): void => {
|
|
158
|
+
if (!isActive()) return;
|
|
159
|
+
clearInitTimeout();
|
|
160
|
+
setErrorState(err);
|
|
161
|
+
setIsLoading(false);
|
|
162
|
+
},
|
|
163
|
+
[isActive, clearInitTimeout, setErrorState, setIsLoading]
|
|
164
|
+
);
|
|
165
|
+
|
|
124
166
|
const handleError = useCallback(
|
|
125
|
-
(
|
|
126
|
-
|
|
127
|
-
const { nativeEvent } = syntheticEvent;
|
|
128
|
-
const err: FincraPaymentError = {
|
|
167
|
+
({ nativeEvent }: WebViewErrorEvent): void => {
|
|
168
|
+
showLoadError({
|
|
129
169
|
code: String(nativeEvent.code ?? 'webview_error'),
|
|
130
170
|
message: nativeEvent.description ?? 'A WebView error occurred.',
|
|
131
|
-
};
|
|
132
|
-
setErrorState(err);
|
|
133
|
-
setIsLoading(false);
|
|
171
|
+
});
|
|
134
172
|
},
|
|
135
|
-
[
|
|
173
|
+
[showLoadError]
|
|
136
174
|
);
|
|
137
175
|
|
|
138
|
-
// ── HTTP error ──────────────────────────────────────────────────────────────
|
|
139
176
|
const handleHttpError = useCallback(
|
|
140
|
-
(
|
|
141
|
-
|
|
142
|
-
const { nativeEvent } = syntheticEvent;
|
|
143
|
-
const err: FincraPaymentError = {
|
|
177
|
+
({ nativeEvent }: WebViewHttpErrorEvent): void => {
|
|
178
|
+
showLoadError({
|
|
144
179
|
code: String(nativeEvent.statusCode ?? 'http_error'),
|
|
145
180
|
message: nativeEvent.description ?? 'A WebView HTTP error occurred.',
|
|
146
|
-
};
|
|
147
|
-
setErrorState(err);
|
|
148
|
-
setIsLoading(false);
|
|
181
|
+
});
|
|
149
182
|
},
|
|
150
|
-
[
|
|
183
|
+
[showLoadError]
|
|
151
184
|
);
|
|
152
185
|
|
|
153
186
|
// ── Retry handler ───────────────────────────────────────────────────────────
|
|
@@ -158,47 +191,42 @@ export function FincraInlineCheckout({
|
|
|
158
191
|
}, [setErrorState, setIsLoading, setReloadKey]);
|
|
159
192
|
|
|
160
193
|
// ── JS Bridge message handler ───────────────────────────────────────────────
|
|
194
|
+
// Messages that arrive after the session settled (e.g. closed via the
|
|
195
|
+
// Android back button) or after unmount are ignored.
|
|
161
196
|
const handleMessage = useCallback(
|
|
162
197
|
(event: WebViewMessageEvent) => {
|
|
163
|
-
if (
|
|
198
|
+
if (!isActive()) return;
|
|
164
199
|
|
|
165
200
|
const msg = parseMessage(event.nativeEvent.data);
|
|
166
201
|
|
|
167
202
|
switch (msg.event) {
|
|
168
203
|
case FincraBridgeEvent.Ready:
|
|
169
204
|
// SDK loaded — clear timeout and hide the loading spinner
|
|
170
|
-
|
|
205
|
+
clearInitTimeout();
|
|
171
206
|
setIsLoading(false);
|
|
172
207
|
break;
|
|
173
208
|
|
|
174
|
-
case FincraBridgeEvent.Success:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
onSuccess?.(msg.data as FincraPaymentResponse);
|
|
179
|
-
} else {
|
|
180
|
-
onCancelled?.();
|
|
181
|
-
}
|
|
209
|
+
case FincraBridgeEvent.Success: {
|
|
210
|
+
// Always a success, even without data (empty references).
|
|
211
|
+
const response = msg.data as FincraPaymentResponse;
|
|
212
|
+
settle(() => onSuccess?.(response));
|
|
182
213
|
break;
|
|
214
|
+
}
|
|
183
215
|
|
|
184
216
|
case FincraBridgeEvent.Closed:
|
|
185
|
-
|
|
186
|
-
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
187
|
-
onCancelled?.();
|
|
217
|
+
settle(() => onCancelled?.());
|
|
188
218
|
break;
|
|
189
219
|
|
|
190
220
|
case FincraBridgeEvent.Error: {
|
|
191
|
-
hasCompleted.current = true;
|
|
192
|
-
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
193
221
|
const errorMessage =
|
|
194
|
-
msg.data && 'message' in msg.data
|
|
195
|
-
?
|
|
222
|
+
msg.data && 'message' in msg.data && msg.data.message
|
|
223
|
+
? msg.data.message
|
|
196
224
|
: 'An unknown error occurred';
|
|
197
225
|
const err: FincraPaymentError = {
|
|
198
226
|
code: 'fincra_sdk_error',
|
|
199
227
|
message: errorMessage,
|
|
200
228
|
};
|
|
201
|
-
onFailed?.(err);
|
|
229
|
+
settle(() => onFailed?.(err));
|
|
202
230
|
break;
|
|
203
231
|
}
|
|
204
232
|
|
|
@@ -208,13 +236,21 @@ export function FincraInlineCheckout({
|
|
|
208
236
|
break;
|
|
209
237
|
}
|
|
210
238
|
},
|
|
211
|
-
[
|
|
239
|
+
[
|
|
240
|
+
isActive,
|
|
241
|
+
clearInitTimeout,
|
|
242
|
+
settle,
|
|
243
|
+
setIsLoading,
|
|
244
|
+
onSuccess,
|
|
245
|
+
onFailed,
|
|
246
|
+
onCancelled,
|
|
247
|
+
]
|
|
212
248
|
);
|
|
213
249
|
|
|
214
250
|
// ── Cancellation ────────────────────────────────────────────────────────────
|
|
215
251
|
// Fix #4: Use static Alert import — no dynamic require() needed.
|
|
216
252
|
const handleCancellation = useCallback(() => {
|
|
217
|
-
if (
|
|
253
|
+
if (!isActive()) return;
|
|
218
254
|
|
|
219
255
|
if (showCancelConfirmationDialog) {
|
|
220
256
|
Alert.alert(
|
|
@@ -225,22 +261,15 @@ export function FincraInlineCheckout({
|
|
|
225
261
|
{
|
|
226
262
|
text: 'Yes',
|
|
227
263
|
style: 'destructive',
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
hasCompleted.current = true;
|
|
231
|
-
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
232
|
-
onCancelled?.();
|
|
233
|
-
}
|
|
234
|
-
},
|
|
264
|
+
// May fire after unmount — settle() ignores it then.
|
|
265
|
+
onPress: () => settle(() => onCancelled?.()),
|
|
235
266
|
},
|
|
236
267
|
]
|
|
237
268
|
);
|
|
238
269
|
} else {
|
|
239
|
-
|
|
240
|
-
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
241
|
-
onCancelled?.();
|
|
270
|
+
settle(() => onCancelled?.());
|
|
242
271
|
}
|
|
243
|
-
}, [showCancelConfirmationDialog, onCancelled]);
|
|
272
|
+
}, [isActive, showCancelConfirmationDialog, settle, onCancelled]);
|
|
244
273
|
|
|
245
274
|
// Keep the ref in sync with the latest handleCancellation (Fix #10)
|
|
246
275
|
useEffect(() => {
|
|
@@ -252,105 +281,116 @@ export function FincraInlineCheckout({
|
|
|
252
281
|
headerTintColor === '#000000' ? 'dark-content' : 'light-content';
|
|
253
282
|
|
|
254
283
|
// ── Render ──────────────────────────────────────────────────────────────────
|
|
284
|
+
// Own SafeAreaProvider: SafeAreaView reads insets from the nearest provider,
|
|
285
|
+
// and a Modal (FincraCheckoutHost) is a separate native tree. Without one the
|
|
286
|
+
// insets are 0 and the header sits under the status bar / Dynamic Island,
|
|
287
|
+
// where iOS swallows taps. Nested providers are fine if the app has its own.
|
|
255
288
|
return (
|
|
256
|
-
<
|
|
257
|
-
<
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
<TouchableOpacity
|
|
267
|
-
style={styles.closeButton}
|
|
268
|
-
onPress={handleCancellation}
|
|
269
|
-
accessibilityLabel="Close checkout"
|
|
270
|
-
accessibilityRole="button"
|
|
271
|
-
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
289
|
+
<SafeAreaProvider style={styles.provider}>
|
|
290
|
+
<SafeAreaView style={styles.container}>
|
|
291
|
+
<StatusBar
|
|
292
|
+
barStyle={statusBarStyle}
|
|
293
|
+
backgroundColor={headerBackgroundColor}
|
|
294
|
+
/>
|
|
295
|
+
|
|
296
|
+
{/* ── Header bar — shown during loading so the user can abort ── */}
|
|
297
|
+
<View
|
|
298
|
+
style={[styles.header, { backgroundColor: headerBackgroundColor }]}
|
|
272
299
|
>
|
|
273
|
-
{
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
300
|
+
{/* Spacer keeps the title centred when the close button is hidden */}
|
|
301
|
+
{showCloseButton ? (
|
|
302
|
+
<TouchableOpacity
|
|
303
|
+
style={styles.closeButton}
|
|
304
|
+
onPress={handleCancellation}
|
|
305
|
+
accessibilityLabel="Close checkout"
|
|
306
|
+
accessibilityRole="button"
|
|
307
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
308
|
+
>
|
|
309
|
+
{closeIcon ?? (
|
|
310
|
+
<Text style={[styles.closeIcon, { color: headerTintColor }]}>
|
|
311
|
+
✕
|
|
312
|
+
</Text>
|
|
313
|
+
)}
|
|
314
|
+
</TouchableOpacity>
|
|
315
|
+
) : (
|
|
316
|
+
<View style={styles.closeButton} />
|
|
317
|
+
)}
|
|
318
|
+
<Text
|
|
319
|
+
style={[styles.headerTitle, { color: headerTintColor }]}
|
|
320
|
+
numberOfLines={1}
|
|
321
|
+
>
|
|
322
|
+
{headerTitle}
|
|
323
|
+
</Text>
|
|
324
|
+
<View style={styles.closeButton} />
|
|
325
|
+
</View>
|
|
326
|
+
|
|
327
|
+
{/* ── WebView running Fincra inline JS SDK ── */}
|
|
328
|
+
<View style={styles.webViewContainer}>
|
|
329
|
+
<WebView
|
|
330
|
+
key={reloadKey}
|
|
331
|
+
ref={webViewRef}
|
|
332
|
+
// Fix #14: restrict to HTTPS + about:blank only (removed wildcard)
|
|
333
|
+
originWhitelist={['https://*', 'about:blank']}
|
|
334
|
+
source={{ html }}
|
|
335
|
+
style={styles.webView}
|
|
336
|
+
javaScriptEnabled
|
|
337
|
+
domStorageEnabled
|
|
338
|
+
onMessage={handleMessage}
|
|
339
|
+
onError={handleError}
|
|
340
|
+
onHttpError={handleHttpError}
|
|
341
|
+
// Inject the ReactNativeWebView bridge shim so older WKWebView versions work
|
|
342
|
+
injectedJavaScriptBeforeContentLoaded={WEBVIEW_BRIDGE_SHIM}
|
|
343
|
+
// Allow the external CDN script to load
|
|
344
|
+
mixedContentMode="always"
|
|
345
|
+
// Fix #15: removed allowFileAccess and allowUniversalAccessFromFileURLs —
|
|
346
|
+
// the HTML is served as an inline blob, not a file:// URL, so these are
|
|
347
|
+
// unnecessary and allowUniversalAccessFromFileURLs is a security footgun.
|
|
348
|
+
/>
|
|
349
|
+
|
|
350
|
+
{/* ── Loading overlay ── */}
|
|
351
|
+
{isLoading && !errorState && (
|
|
352
|
+
<View style={styles.loadingOverlay}>
|
|
353
|
+
<View style={styles.loadingCard}>
|
|
354
|
+
{loadingComponent ?? (
|
|
355
|
+
<ActivityIndicator size="large" color="#0066FF" />
|
|
356
|
+
)}
|
|
357
|
+
</View>
|
|
358
|
+
</View>
|
|
277
359
|
)}
|
|
278
|
-
</TouchableOpacity>
|
|
279
|
-
<Text
|
|
280
|
-
style={[styles.headerTitle, { color: headerTintColor }]}
|
|
281
|
-
numberOfLines={1}
|
|
282
|
-
>
|
|
283
|
-
{headerTitle}
|
|
284
|
-
</Text>
|
|
285
|
-
<View style={styles.closeButton} />
|
|
286
|
-
</View>
|
|
287
|
-
|
|
288
|
-
{/* ── WebView running Fincra inline JS SDK ── */}
|
|
289
|
-
<View style={styles.webViewContainer}>
|
|
290
|
-
<WebView
|
|
291
|
-
key={reloadKey}
|
|
292
|
-
ref={webViewRef}
|
|
293
|
-
// Fix #14: restrict to HTTPS + about:blank only (removed wildcard)
|
|
294
|
-
originWhitelist={['https://*', 'about:blank']}
|
|
295
|
-
source={{ html }}
|
|
296
|
-
style={styles.webView}
|
|
297
|
-
javaScriptEnabled
|
|
298
|
-
domStorageEnabled
|
|
299
|
-
onMessage={handleMessage}
|
|
300
|
-
onError={handleError}
|
|
301
|
-
onHttpError={handleHttpError}
|
|
302
|
-
// Inject the ReactNativeWebView bridge shim so older WKWebView versions work
|
|
303
|
-
injectedJavaScriptBeforeContentLoaded={WEBVIEW_BRIDGE_SHIM}
|
|
304
|
-
// Allow the external CDN script to load
|
|
305
|
-
mixedContentMode="always"
|
|
306
|
-
// Fix #15: removed allowFileAccess and allowUniversalAccessFromFileURLs —
|
|
307
|
-
// the HTML is served as an inline blob, not a file:// URL, so these are
|
|
308
|
-
// unnecessary and allowUniversalAccessFromFileURLs is a security footgun.
|
|
309
|
-
/>
|
|
310
360
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
361
|
+
{/* ── Error Recovery overlay ── */}
|
|
362
|
+
{errorState && (
|
|
363
|
+
<View style={styles.errorOverlay}>
|
|
364
|
+
{renderError ? (
|
|
365
|
+
renderError(errorState, handleRetry)
|
|
366
|
+
) : (
|
|
367
|
+
<View style={styles.errorContainer}>
|
|
368
|
+
<Text style={styles.errorIcon}>⚠️</Text>
|
|
369
|
+
<Text style={styles.errorTitle}>Connection Error</Text>
|
|
370
|
+
<Text style={styles.errorMessage}>{errorState.message}</Text>
|
|
371
|
+
<TouchableOpacity
|
|
372
|
+
style={styles.retryButton}
|
|
373
|
+
onPress={handleRetry}
|
|
374
|
+
accessibilityRole="button"
|
|
375
|
+
accessibilityLabel="Retry loading checkout"
|
|
376
|
+
>
|
|
377
|
+
<Text style={styles.retryButtonText}>Retry</Text>
|
|
378
|
+
</TouchableOpacity>
|
|
379
|
+
<TouchableOpacity
|
|
380
|
+
style={styles.cancelButton}
|
|
381
|
+
onPress={handleCancellation}
|
|
382
|
+
accessibilityRole="button"
|
|
383
|
+
accessibilityLabel="Cancel checkout"
|
|
384
|
+
>
|
|
385
|
+
<Text style={styles.cancelButtonText}>Cancel</Text>
|
|
386
|
+
</TouchableOpacity>
|
|
387
|
+
</View>
|
|
317
388
|
)}
|
|
318
389
|
</View>
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
{errorState && (
|
|
324
|
-
<View style={styles.errorOverlay}>
|
|
325
|
-
{renderError ? (
|
|
326
|
-
renderError(errorState, handleRetry)
|
|
327
|
-
) : (
|
|
328
|
-
<View style={styles.errorContainer}>
|
|
329
|
-
<Text style={styles.errorIcon}>⚠️</Text>
|
|
330
|
-
<Text style={styles.errorTitle}>Connection Error</Text>
|
|
331
|
-
<Text style={styles.errorMessage}>{errorState.message}</Text>
|
|
332
|
-
<TouchableOpacity
|
|
333
|
-
style={styles.retryButton}
|
|
334
|
-
onPress={handleRetry}
|
|
335
|
-
accessibilityRole="button"
|
|
336
|
-
accessibilityLabel="Retry loading checkout"
|
|
337
|
-
>
|
|
338
|
-
<Text style={styles.retryButtonText}>Retry</Text>
|
|
339
|
-
</TouchableOpacity>
|
|
340
|
-
<TouchableOpacity
|
|
341
|
-
style={styles.cancelButton}
|
|
342
|
-
onPress={handleCancellation}
|
|
343
|
-
accessibilityRole="button"
|
|
344
|
-
accessibilityLabel="Cancel checkout"
|
|
345
|
-
>
|
|
346
|
-
<Text style={styles.cancelButtonText}>Cancel</Text>
|
|
347
|
-
</TouchableOpacity>
|
|
348
|
-
</View>
|
|
349
|
-
)}
|
|
350
|
-
</View>
|
|
351
|
-
)}
|
|
352
|
-
</View>
|
|
353
|
-
</SafeAreaView>
|
|
390
|
+
)}
|
|
391
|
+
</View>
|
|
392
|
+
</SafeAreaView>
|
|
393
|
+
</SafeAreaProvider>
|
|
354
394
|
);
|
|
355
395
|
}
|
|
356
396
|
|
|
@@ -374,6 +414,9 @@ const WEBVIEW_BRIDGE_SHIM = `
|
|
|
374
414
|
// ─── Styles ────────────────────────────────────────────────────────────────────
|
|
375
415
|
|
|
376
416
|
const styles = StyleSheet.create({
|
|
417
|
+
provider: {
|
|
418
|
+
flex: 1,
|
|
419
|
+
},
|
|
377
420
|
container: {
|
|
378
421
|
flex: 1,
|
|
379
422
|
backgroundColor: '#FFFFFF',
|