import React from 'react'; import { StyleSheet, Modal, View, Animated, TouchableWithoutFeedback, Text, Alert, Image, Dimensions, Easing, TouchableOpacity, Platform, } from 'react-native'; import WebView from 'react-native-webview'; import {WebViewNavigation} from 'react-native-webview/lib/WebViewTypes'; import {colors} from './configs'; const loader = require('./assets/loader.gif'); const borderRadiusDimension = 24 / 896; const windowHeight = Dimensions.get('window').height; export interface FlutterwaveCheckoutProps { onRedirect?: (data: any) => void; onAbort?: () => void; link?: string; visible?: boolean; } interface FlutterwaveCheckoutBackdropProps { animation: Animated.Value, onPress?: () => void; } interface FlutterwaveCheckoutErrorProps { hasLink: boolean; onTryAgain: () => void; } const getRedirectParams = (url: string): {[k: string]: string} => { // initialize result container const res: any = {}; // if url has params if (url.split('?').length > 1) { // get query params in an array const params = url.split('?')[1].split('&'); // add url params to result for (let i = 0; i < params.length; i++) { const param: Array<string> = params[i].split('='); const val = decodeURIComponent(param[1]).trim(); res[param[0]] = String(val); } } // return result return res; }; const FlutterwaveCheckout: React.FC<FlutterwaveCheckoutProps> = function FlutterwaveCheckout(props) { const {link, visible, onRedirect, onAbort} = props; const [show, setShow] = React.useState<boolean>(false); const webviewRef = React.useRef<WebView | null>(null); const animation = React.useRef<Animated.Value>(new Animated.Value(0)); const animateIn = React.useCallback(() => { setShow(true); Animated.timing(animation.current, { toValue: 1, duration: 700, easing: Easing.in(Easing.elastic(0.72)), useNativeDriver: false, }).start(); }, []); const animateOut = React.useCallback((): Promise<void> => { return new Promise(resolve => { Animated.timing(animation.current, { toValue: 0, duration: 400, useNativeDriver: false, }).start(() => { setShow(false); resolve(); }); }) }, []); const handleReload = React.useCallback(() => { if (webviewRef.current) { webviewRef.current.reload(); } }, []); const handleAbort = React.useCallback((confirmed: boolean = false) => { if (!confirmed) { Alert.alert('', 'Are you sure you want to cancel this payment?', [ {text: 'No'}, { text: 'Yes, Cancel', style: 'destructive', onPress: () => handleAbort(true), }, ]); return; } // remove tx_ref and dismiss animateOut().then(onAbort); }, [onAbort, animateOut]); const handleNavigationStateChange = React.useCallback((ev: WebViewNavigation): boolean => { // cregex to check if redirect has occured on completion/cancel const rx = /\/flutterwave\.com\/rn-redirect/; // Don't end payment if not redirected back if (!rx.test(ev.url)) { return true; } // dismiss modal animateOut().then(() => { if (onRedirect) { onRedirect(getRedirectParams(ev.url)) } }); return false; }, [onRedirect]); const doAnimate = React.useCallback(() => { if (visible === show) { return; } if (visible) { return animateIn(); } animateOut().then(() => {}); }, [visible, show, animateOut, animateIn]); React.useEffect(() => { doAnimate(); return () => {}; }, [doAnimate]); const marginTop = animation.current.interpolate({ inputRange: [0, 1], outputRange: [windowHeight, 0], }); const opacity = animation.current.interpolate({ inputRange: [0, 0.3, 1], outputRange: [0, 1, 1], }); return ( <Modal transparent={true} animated={false} hardwareAccelerated={false} visible={show}> <FlutterwaveCheckoutBackdrop onPress={() => handleAbort()} animation={animation.current} /> <Animated.View style={[ styles.webviewContainer, { marginTop, opacity } ]} testID='flw-checkout-dialog' > <WebView ref={webviewRef} source={{uri: link || ''}} style={styles.webview} startInLoadingState={true} scalesPageToFit={true} javaScriptEnabled={true} onShouldStartLoadWithRequest={handleNavigationStateChange} renderError={() => <FlutterwaveCheckoutError hasLink={!!link} onTryAgain={handleReload} />} renderLoading={() => <FlutterwaveCheckoutLoader />} /> </Animated.View> </Modal> ) } const FlutterwaveCheckoutBackdrop: React.FC< FlutterwaveCheckoutBackdropProps > = function FlutterwaveCheckoutBackdrop({ animation, onPress }) { // Interpolation backdrop animation const backgroundColor = animation.interpolate({ inputRange: [0, 0.3, 1], outputRange: [colors.transparent, colors.transparent, 'rgba(0,0,0,0.5)'], }); return ( <TouchableWithoutFeedback testID='flw-checkout-backdrop' onPress={onPress}> <Animated.View style={Object.assign({}, styles.backdrop, {backgroundColor})} /> </TouchableWithoutFeedback> ); } export const FlutterwaveCheckoutError: React.FC<FlutterwaveCheckoutErrorProps> = ({ hasLink, onTryAgain }): React.ReactElement => { return ( <View style={styles.error} testID="flw-checkout-error"> {hasLink ? ( <> <Text style={styles.errorText}> An error occurred, please tab below to try again. </Text> <TouchableOpacity style={styles.errorActionButton} onPress={onTryAgain}> <Text style={styles.errorActionButtonText}>Try Again</Text> </TouchableOpacity> </> ) : ( <Text style={styles.errorText}> An error occurred, please close the checkout dialog and try again. </Text> )} </View> ); } const FlutterwaveCheckoutLoader: React.FC<{}> = (): React.ReactElement => { return ( <View style={styles.loading} testID="flw-checkout-loader"> <Image source={loader} resizeMode="contain" style={styles.loadingImage} /> </View> ); } const styles = StyleSheet.create({ errorActionButtonText: { textAlign: 'center', color: colors.primary, fontSize: 16, }, errorActionButton: { paddingHorizontal: 16, paddingVertical: 16, }, errorText: { color: colors.secondary, textAlign: 'center', marginBottom: 32, fontSize: 18, }, error: { position: 'absolute', left: 0, right: 0, bottom: 0, top: 0, backgroundColor: '#ffffff', justifyContent: 'center', alignItems: 'center', paddingHorizontal: 56, }, backdrop: { position: 'absolute', left: 0, right: 0, bottom: 0, top: 0, }, loadingImage: { width: 64, height: 64, resizeMode: 'contain', }, loading: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0, backgroundColor: 'rgba(255, 255, 255, 0.3)', justifyContent: 'center', alignItems: 'center', }, webviewContainer: { top: Platform.select({ios: 96, android: 64}), // status bar height aware for ios flex: 1, backgroundColor: '#efefef', paddingBottom: Platform.select({ios: 96, android: 64}), // status bar height aware for ios overflow: 'hidden', borderTopLeftRadius: windowHeight * borderRadiusDimension, borderTopRightRadius: windowHeight * borderRadiusDimension, }, webview: { flex: 1, backgroundColor: 'rgba(0,0,0,0)', }, }); export default FlutterwaveCheckout;