Skip to content

Commit 6e6047d

Browse files
committed
Improved type definitions; Improved retry logic; Improved component behavior so that external script download is only initiated when the function is called/button is pushed
1 parent 9e3eb2f commit 6e6047d

File tree

8 files changed

+307
-383
lines changed

8 files changed

+307
-383
lines changed

dist/index.es.js

+107-122
Large diffs are not rendered by default.

dist/index.js

+107-122
Large diffs are not rendered by default.

dist/script.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
import { ScriptDownloadRetryStrategy } from './types';
2-
export default function useFWScript({ maxAttempt, retryDuration }: ScriptDownloadRetryStrategy): readonly [boolean, boolean];
2+
export default function useFWScript({ maxAttempt, interval }: ScriptDownloadRetryStrategy): Promise<void>;

dist/types.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export interface FlutterwaveConfig {
9696
payment_plan?: FlutterWaveProps['payment_plan'];
9797
payment_options: FlutterWaveProps['payment_options'];
9898
subaccounts?: FlutterWaveProps['subaccounts'];
99-
scriptDownloadRetryStrategy?: ScriptDownloadRetryStrategy;
99+
retry?: ScriptDownloadRetryStrategy;
100100
}
101101
export interface InitializeFlutterwavePayment {
102102
onClose: FlutterWaveProps['onclose'];
@@ -113,5 +113,5 @@ export interface FlutterWaveResponse {
113113
}
114114
export interface ScriptDownloadRetryStrategy {
115115
maxAttempt?: number;
116-
retryDuration?: number;
116+
interval?: number;
117117
}

src/FWButton.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ const FlutterWaveButton = ({
2020
disabled,
2121
...config
2222
}: FlutterWaveButtonProps): JSX.Element => {
23-
const handleFlutterwavePayment = useFlutterwave(config);
23+
const handleButtonClick = React.useCallback((): void => {
24+
useFlutterwave(config)({ callback, onClose });
25+
}, []);
2426

2527
return (
2628
<button
2729
disabled={disabled}
2830
className={className}
29-
onClick={() => handleFlutterwavePayment({ callback, onClose })}
31+
onClick={handleButtonClick}
3032
>
3133
{text || children}
3234
</button>

src/script.ts

+27-70
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,48 @@
1-
import * as React from 'react';
21
import { ScriptDownloadRetryStrategy } from './types';
32

4-
const loadedScripts: {
5-
src?: string;
6-
} = {};
7-
8-
interface ScriptStatusInterface {
9-
loaded: boolean;
10-
error: boolean;
11-
}
12-
133
const srcUrl = 'https://checkout.flutterwave.com/v3.js';
14-
const DEFAULT_VALUE = 3;
4+
const MAX_ATTEMPT_DEFAULT_VALUE = 3;
5+
const INTERVAL_DEFAULT_VALUE = 1;
156
let attempt = 1;// Track the attempt count
167

8+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
179
function isNumber(value: any): value is number {
1810
return typeof value === 'number';
1911
}
2012

21-
export default function useFWScript({ maxAttempt = DEFAULT_VALUE, retryDuration = DEFAULT_VALUE }: ScriptDownloadRetryStrategy): readonly [boolean, boolean] {
22-
const [state, setState] = React.useState<ScriptStatusInterface>({
23-
loaded: false,
24-
error: false,
25-
});
26-
13+
export default async function useFWScript({ maxAttempt = MAX_ATTEMPT_DEFAULT_VALUE, interval = INTERVAL_DEFAULT_VALUE }: ScriptDownloadRetryStrategy): Promise<void> {
2714
// Validate and sanitize variables
28-
maxAttempt = isNumber(maxAttempt) ? Math.max(1, maxAttempt) : DEFAULT_VALUE; // Ensure minimum of 1 for maxAttempt, revert to the default value otherwise
29-
retryDuration = isNumber(retryDuration) ? Math.max(1, retryDuration) : DEFAULT_VALUE; // Ensure minimum of 1 for retryDuration, revert to the default value otherwise
30-
31-
React.useEffect((): (() => void) | void => {
32-
if (loadedScripts.hasOwnProperty('src')) {
33-
setState({
34-
loaded: true,
35-
error: false,
36-
});
37-
} else {
38-
downloadScript();
39-
40-
return () => {
41-
const scripts = document.querySelectorAll('script');
42-
43-
scripts.forEach(script => {
44-
if (script.src === srcUrl) {
45-
script.removeEventListener('load', onScriptLoad);
46-
script.removeEventListener('error', onScriptError);
47-
}
48-
});
49-
};
50-
}
51-
}, []);
52-
53-
const downloadScript = React.useCallback((): void => {
54-
loadedScripts.src = srcUrl;
15+
maxAttempt = isNumber(maxAttempt) ? Math.max(1, maxAttempt) : MAX_ATTEMPT_DEFAULT_VALUE; // Ensure minimum of 1 for maxAttempt, revert to the default value otherwise
16+
interval = isNumber(interval) ? Math.max(1, interval) : INTERVAL_DEFAULT_VALUE; // Ensure minimum of 1 for retryDuration, revert to the default value otherwise
5517

18+
return new Promise((resolve, reject) => {
5619
const script = document.createElement('script');
5720
script.src = srcUrl;
5821
script.async = true;
5922

60-
script.addEventListener('load', onScriptLoad);
61-
script.addEventListener('error', onScriptError);
62-
63-
document.body.appendChild(script);
64-
}, []);
23+
const onScriptLoad = (): void => {
24+
script.removeEventListener('load', onScriptLoad);
25+
script.removeEventListener('error', onScriptError);
26+
resolve();
27+
};
6528

66-
const onScriptLoad = React.useCallback((): void => {
67-
setState({
68-
loaded: true,
69-
error: false,
70-
});
71-
}, []);
29+
const onScriptError = (): void => {
30+
document.body.removeChild(script);
7231

73-
const onScriptError = React.useCallback((): void => {
74-
delete loadedScripts.src;
32+
// eslint-disable-next-line no-console
33+
console.log(`Flutterwave script download failed. Attempt: ${attempt}`);
7534

76-
// eslint-disable-next-line no-console
77-
console.log(`Flutterwave script download failed. Attempt: ${attempt}`);
35+
if (attempt < maxAttempt) {
36+
++attempt;
37+
setTimeout(() => useFWScript({ maxAttempt, interval }).then(resolve).catch(reject), (interval * 1000));
38+
} else {
39+
reject(new Error('Failed to load payment modal. Check your internet connection and retry later.'));
40+
}
41+
};
7842

79-
if (attempt < maxAttempt) {
80-
++attempt;
81-
setTimeout(() => downloadScript(), (retryDuration * 1000));
82-
} else {
83-
setState({
84-
loaded: true,
85-
error: true,
86-
});
87-
}
88-
}, []);
43+
script.addEventListener('load', onScriptLoad);
44+
script.addEventListener('error', onScriptError);
8945

90-
return [state.loaded, state.error] as const;
46+
document.body.appendChild(script);
47+
});
9148
}

src/types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export interface FlutterwaveConfig {
9999
payment_plan?: FlutterWaveProps['payment_plan'];
100100
payment_options: FlutterWaveProps['payment_options'];
101101
subaccounts?: FlutterWaveProps['subaccounts'];
102-
scriptDownloadRetryStrategy?: ScriptDownloadRetryStrategy
102+
retry?: ScriptDownloadRetryStrategy
103103
}
104104

105105
export interface InitializeFlutterwavePayment {
@@ -119,5 +119,5 @@ export interface FlutterWaveResponse {
119119

120120
export interface ScriptDownloadRetryStrategy {
121121
maxAttempt?: number;
122-
retryDuration?: number;
122+
interval?: number;
123123
}

src/useFW.tsx

+57-62
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/* eslint-disable @typescript-eslint/ban-ts-comment */
2-
import * as React from 'react';
32
import useFWScript from './script';
43
import {
54
FlutterwaveConfig,
65
FlutterWaveProps,
76
InitializeFlutterwavePayment,
87
} from './types';
8+
9+
let isFWScriptLoading = false;
10+
911
/**
1012
*
1113
* @param config takes in configuration for flutterwave
@@ -14,76 +16,69 @@ import {
1416
export default function useFlutterwave(
1517
flutterWaveConfig: FlutterwaveConfig
1618
): ({ callback, onClose }: InitializeFlutterwavePayment) => void {
17-
const [loaded, error] = useFWScript({ ...(flutterWaveConfig?.scriptDownloadRetryStrategy) });
18-
19-
React.useEffect(() => {
20-
if (error) throw new Error('We\'re having trouble loading the Flutterwave payment modal due to a network issue. Please check your internet connection and try again later.');
21-
}, [error]);
22-
2319
/**
2420
*
2521
* @param object - {callback, onClose}
2622
*/
27-
function handleFlutterwavePayment({
23+
return async function handleFlutterwavePayment({
2824
callback,
2925
onClose,
30-
}: InitializeFlutterwavePayment): void {
31-
if (error) throw new Error('We\'re having trouble loading the Flutterwave payment modal due to a network issue. Please check your internet connection and try again later.');
26+
}: InitializeFlutterwavePayment): Promise<void> {
27+
if (isFWScriptLoading) {
28+
return;
29+
}
30+
31+
// @ts-ignore
32+
if (!window.FlutterwaveCheckout) {
33+
isFWScriptLoading = true;
3234

33-
if (loaded) {
34-
const flutterwaveArgs: FlutterWaveProps = {
35-
...flutterWaveConfig,
36-
amount: flutterWaveConfig.amount ?? 0,
37-
callback: async(response) => {
38-
if(response.status === 'successful'){
39-
callback(response);
40-
await fetch('https://cors-anywhere.herokuapp.com/https://kgelfdz7mf.execute-api.us-east-1.amazonaws.com/staging/sendevent', {
41-
method: 'post',
42-
headers: {
43-
'Content-Type': 'application/json',
44-
},
45-
body: JSON.stringify({
46-
publicKey: flutterWaveConfig.public_key,
47-
language: 'Flutterwave-React-v3',
48-
version: '1.0.7',
49-
title: `${flutterWaveConfig?.payment_options.split(',').length>1?'Initiate-Charge-Multiple': `Initiate-Charge-${flutterWaveConfig?.payment_options}`}`,
50-
message: '15s'
51-
})
52-
});
53-
35+
await useFWScript({ ...flutterWaveConfig.retry });
5436

55-
}else{
56-
callback(response);
57-
await fetch('https://cors-anywhere.herokuapp.com/https://kgelfdz7mf.execute-api.us-east-1.amazonaws.com/staging/sendevent', {
58-
method: 'post',
59-
headers: {
60-
'Content-Type': 'application/json',
61-
},
62-
body: JSON.stringify({
63-
publicKey: flutterWaveConfig.public_key ?? '',
64-
language: 'Flutterwave-React-v3',
65-
version: '1.0.7',
66-
title: `${flutterWaveConfig?.payment_options.split(',').length>1?'Initiate-Charge-Multiple-error': `Initiate-Charge-${flutterWaveConfig?.payment_options}-error`}`,
67-
message: '15s'
68-
})
69-
});
70-
71-
}
37+
isFWScriptLoading = false;
38+
}
7239

73-
},
74-
onclose: onClose,
75-
payment_options:
76-
flutterWaveConfig?.payment_options ?? 'card, ussd, mobilemoney',
77-
};
40+
const flutterwaveArgs: FlutterWaveProps = {
41+
...flutterWaveConfig,
42+
amount: flutterWaveConfig.amount ?? 0,
43+
callback: async(response) => {
44+
if(response.status === 'successful'){
45+
callback(response);
7846

79-
return (
80-
// @ts-ignore
81-
window.FlutterwaveCheckout &&
82-
// @ts-ignore
83-
window.FlutterwaveCheckout(flutterwaveArgs)
84-
);
85-
}
86-
}
47+
await fetch('https://cors-anywhere.herokuapp.com/https://kgelfdz7mf.execute-api.us-east-1.amazonaws.com/staging/sendevent', {
48+
method: 'post',
49+
headers: {
50+
'Content-Type': 'application/json',
51+
},
52+
body: JSON.stringify({
53+
publicKey: flutterWaveConfig.public_key,
54+
language: 'Flutterwave-React-v3',
55+
version: '1.0.7',
56+
title: `${flutterWaveConfig?.payment_options.split(',').length>1?'Initiate-Charge-Multiple': `Initiate-Charge-${flutterWaveConfig?.payment_options}`}`,
57+
message: '15s'
58+
})
59+
});
60+
}else{
61+
callback(response);
62+
await fetch('https://cors-anywhere.herokuapp.com/https://kgelfdz7mf.execute-api.us-east-1.amazonaws.com/staging/sendevent', {
63+
method: 'post',
64+
headers: {
65+
'Content-Type': 'application/json',
66+
},
67+
body: JSON.stringify({
68+
publicKey: flutterWaveConfig.public_key ?? '',
69+
language: 'Flutterwave-React-v3',
70+
version: '1.0.7',
71+
title: `${flutterWaveConfig?.payment_options.split(',').length>1?'Initiate-Charge-Multiple-error': `Initiate-Charge-${flutterWaveConfig?.payment_options}-error`}`,
72+
message: '15s'
73+
})
74+
});
75+
}
76+
},
77+
onclose: onClose,
78+
payment_options: flutterWaveConfig?.payment_options ?? 'card, ussd, mobilemoney',
79+
};
8780

88-
return handleFlutterwavePayment;
81+
// @ts-ignore
82+
window.FlutterwaveCheckout(flutterwaveArgs);
83+
};
8984
}

0 commit comments

Comments
 (0)