import React, { useState, useEffect, useRef } from 'react'; import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons'; import { View, StyleSheet, Platform, I18nManager, ScrollView, TouchableOpacity } from 'react-native'; import { initStripe, useConfirmPayment } from '@stripe/stripe-react-native'; import Picker from 'react-native-country-picker-modal'; import MaterialIcons from 'react-native-vector-icons/MaterialIcons' import { Checkout as CheckoutController, useOrder, useSession, useApi, useLanguage, useUtils, useValidationFields, useConfig, ToastType, useToast } from 'ordering-components/native'; import { OText, OButton, OIcon } from '../shared'; import { AddressDetails } from '../AddressDetails'; import { PaymentOptions } from '../PaymentOptions'; import { DriverTips } from '../DriverTips'; import { OrderSummary } from '../OrderSummary'; import { NotFoundSource } from '../NotFoundSource'; import { UserDetails } from '../UserDetails'; import { OrderTypeSelector } from '../OrderTypeSelector' import { ChContainer, ChSection, ChHeader, ChTotal, ChAddress, ChMoment, CHMomentWrapper, ChPaymethods, ChDriverTips, ChCart, ChErrors, ChBusinessDetails, ChUserDetails, TextDetails, DeliveryOptionsContainer, DeliveryOptionItem } from './styles'; import { Fade, Placeholder, PlaceholderLine } from 'rn-placeholder'; import { FloatingButton } from '../FloatingButton'; import { Container } from '../../layouts/Container'; import { useTheme } from 'styled-components/native'; import { ActivityIndicator } from 'react-native-paper'; import WebView from 'react-native-webview'; import Icon from 'react-native-vector-icons/Feather'; import { OrderCreating } from '../OrderCreating'; import { PaymentOptionsWebView } from '../PaymentOptionsWebView'; const mapConfigs = { mapZoom: 16, mapSize: { width: 640, height: 190 } }; const manageErrorsToShow = (array = []) => { let stringError = '' const list = Array.isArray(array) ? array : Object.values(array) list.map((item: any, i: number) => { stringError += (i + 1) === array.length ? `- ${item?.message || item}` : `- ${item?.message || item}\n` }) return stringError; } const CheckoutUI = (props: any) => { const { navigation, cart, errors, placing, cartState, businessDetails, paymethodSelected, handlePaymethodChange, handlerClickPlaceOrder, onNavigationRedirect, businessLogo, businessName, cartTotal, currency, deliveryOptionSelected, instructionsOptions, handleChangeDeliveryOption, merchantId } = props const theme = useTheme(); const style = StyleSheet.create({ btnBackArrow: { borderWidth: 0, backgroundColor: theme.colors.white, borderColor: theme.colors.white, shadowColor: theme.colors.white, display: 'flex', justifyContent: 'flex-start', paddingLeft: 0, }, paddSection: { padding: 20 }, paddSectionH: { paddingHorizontal: 20 }, icon: { top: 15, right: Platform.OS === 'ios' ? 5 : (I18nManager.isRTL ? 30 : 0), position: 'absolute', fontSize: 20 } }) const [, { showToast }] = useToast(); const [, t] = useLanguage(); const [{ user, token }] = useSession(); const [{ configs }] = useConfig(); const [{ parsePrice, parseDate }] = useUtils(); const [{ options, carts, loading }, { confirmCart }] = useOrder(); const [validationFields] = useValidationFields(); const [ordering] = useApi() const [errorCash, setErrorCash] = useState(false); const [userErrors, setUserErrors] = useState([]); const [isUserDetailsEdit, setIsUserDetailsEdit] = useState(false); const [phoneUpdate, setPhoneUpdate] = useState(false); const [showGateway, setShowGateway] = useState({ closedByUsed: false, open: false }); const [webviewPaymethod, setWebviewPaymethod] = useState(null) const [openOrderCreating, setOpenOrderCreating] = useState(false) const [cardData, setCardData] = useState(null) const [isDeliveryOptionModalVisible, setIsDeliveryOptionModalVisible] = useState(false) const driverTipsOptions = typeof configs?.driver_tip_options?.value === 'string' ? JSON.parse(configs?.driver_tip_options?.value) || [] : configs?.driver_tip_options?.value || [] const configTypes = configs?.order_types_allowed?.value.split('|').map((value: any) => Number(value)) || [] const isPreOrderSetting = configs?.preorder_status_enabled?.value === '1' const cartsWithProducts = carts && Object.values(carts).filter((cart: any) => cart.products.length) || null const deliveryOptions = instructionsOptions?.result && instructionsOptions?.result?.filter((option: any) => option?.enabled)?.map((option: any) => { return { value: option?.id, key: option?.id, label: t(option?.name.toUpperCase().replace(/\s/g, '_'), option?.name) } }) const handlePlaceOrder = () => { if (!userErrors.length) { setOpenOrderCreating(true) handlerClickPlaceOrder && handlerClickPlaceOrder() return } let stringError = '' Object.values(userErrors).map((item: any, i: number) => { stringError += (i + 1) === userErrors.length ? `- ${item?.message || item}` : `- ${item?.message || item}\n` }) showToast(ToastType.Error, stringError) setIsUserDetailsEdit(true) } const checkValidationFields = () => { setUserErrors([]) const errors = [] const notFields = ['coupon', 'driver_tip', 'mobile_phone', 'address', 'zipcode', 'address_notes'] Object.values(validationFields?.fields?.checkout).map((field: any) => { if (field?.required && !notFields.includes(field.code)) { if (!user[field?.code]) { errors.push(t(`VALIDATION_ERROR_${field.code.toUpperCase()}_REQUIRED`, `The field ${field?.name} is required`)) } } }) if ( !user?.cellphone && validationFields?.fields?.checkout?.cellphone?.enabled && validationFields?.fields?.checkout?.cellphone?.required ) { errors.push(t('VALIDATION_ERROR_MOBILE_PHONE_REQUIRED', 'The field Phone number is required')) } if (phoneUpdate) { errors.push(t('NECESSARY_UPDATE_COUNTRY_PHONE_CODE', 'It is necessary to update your phone number')) } setUserErrors(errors) } const togglePhoneUpdate = (val: boolean) => { setPhoneUpdate(val) } const onFailPaypal = async () => { if (showGateway.closedByUser === true) { await confirmCart(cart.uuid) } } const handlePaymentMethodClick = (paymethod: any) => { setShowGateway({ closedByUser: false, open: true }) setWebviewPaymethod(paymethod) } const changeDeliveryOption = (option: any) => { handleChangeDeliveryOption(option) setIsDeliveryOptionModalVisible(false) } useEffect(() => { if (validationFields && validationFields?.fields?.checkout) { checkValidationFields() } }, [validationFields, user]) useEffect(() => { if (errors) { const errorText = manageErrorsToShow(errors) showToast(ToastType.Error, errorText) setOpenOrderCreating(false) } }, [errors]) useEffect(() => { onFailPaypal() }, [showGateway.closedByUser]) return ( <> navigation?.canGoBack() && navigation.goBack()} /> {t('CHECKOUT', 'Checkout')} {!cartState.loading && (cart?.status === 2 || cart?.status === 4) && ( {!cartState.loading && cart?.status === 2 && ( {t('CART_STATUS_PENDING_MESSAGE_APP', 'Your order is being processed, please wait a little more. if you\'ve been waiting too long, please reload the app')} )} )} {businessName || businessDetails?.business?.name} {!cartState.loading && ( {parsePrice(cart?.total >= 0 ? cart?.total : 0) || parsePrice(cartTotal >= 0 ? cartTotal : 0)} )} {(businessDetails?.loading || cartState.loading) ? ( ) : ( )} {(isPreOrderSetting || configs?.preorder_status_enabled?.value === undefined) && ( {cartState.loading ? ( ) : ( navigation.navigate('MomentOption')} > {options?.moment ? parseDate(options?.moment, { outputFormat: configs?.dates_moment_format?.value }) : t('ASAP_ABBREVIATION', 'ASAP')} )} )} {cartState.loading ? ( ) : ( )} { (businessDetails?.loading || cartState.loading) && !businessDetails?.error && ( )} { !cartState.loading && businessDetails?.business && Object.values(businessDetails?.business).length > 0 && ( {t('BUSINESS_DETAILS', 'Business Details')} {t('NAME', 'Name')}:{' '} {businessDetails?.business?.name} {t('EMAIL', 'Email')}:{' '} {businessDetails?.business?.email} {t('CELLPHONE', 'Cellphone')}:{' '} {businessDetails?.business?.cellphone} {t('ADDRESS', 'Address')}:{' '} {businessDetails?.business?.address} )} {businessDetails?.error && businessDetails?.error?.length > 0 && ( {t('BUSINESS_DETAILS', 'Business Details')} )} {!cartState.loading && deliveryOptionSelected !== undefined && options?.type === 1 && ( {t('DELIVERY_OPTIONS', 'Delivery options')} setIsDeliveryOptionModalVisible(false)} withCountryNameButton renderFlagButton={() => ( setIsDeliveryOptionModalVisible(true)}> {deliveryOptions.find((option: any) => option.value === deliveryOptionSelected).label} )} flatListProps={{ keyExtractor: (item: any) => item.value, data: deliveryOptions || [], renderItem: ({ item }: any) => ( changeDeliveryOption(item.value)} disabled={ deliveryOptionSelected === item.value } > {item.label} ) }} /> )} {!cartState.loading && cart && cart?.valid && options.type === 1 && cart?.status !== 2 && validationFields?.fields?.checkout?.driver_tip?.enabled && driverTipsOptions && driverTipsOptions?.length > 0 && ( {t('DRIVER_TIPS', 'Driver Tips')} )} {!cartState.loading && cart && cart?.status !== 2 && cart?.valid && ( {t('PAYMENT_METHOD', 'Payment Method')} {!cartState.loading && cart?.status === 4 && ( {t('CART_STATUS_CANCEL_MESSAGE', 'The payment has not been successful, please try again')} )} )} {!cartState.loading && cart && ( {cartsWithProducts && cart?.products?.length === 0 ? ( ) : ( <> {t('ORDER_SUMMARY', 'Order Summary')} )} )} {!cartState.loading && cart && ( {!cart?.valid_address && cart?.status !== 2 && ( {t('INVALID_CART_ADDRESS', 'Selected address is invalid, please select a closer address.')} )} {!paymethodSelected && cart?.status !== 2 && cart?.valid && ( {t('WARNING_NOT_PAYMENT_SELECTED', 'Please, select a payment method to place order.')} )} {!cart?.valid_products && cart?.status !== 2 && ( {t('WARNING_INVALID_PRODUCTS', 'Some products are invalid, please check them.')} )} {options.type === 1 && validationFields?.fields?.checkout?.driver_tip?.enabled && validationFields?.fields?.checkout?.driver_tip?.required && (Number(cart?.driver_tip) <= 0) && ( {t('WARNING_INVALID_DRIVER_TIP', 'Driver Tip is required.')} )} )} {!cartState.loading && cart && cart?.status !== 2 && ( <> <> handlePlaceOrder()} isSecondaryBtn={loading || !cart?.valid || !paymethodSelected || placing || errorCash || cart?.subtotal_to_calculate < cart?.minimum || paymethodSelected?.gateway === 'paypal' || (options.type === 1 && validationFields?.fields?.checkout?.driver_tip?.enabled && validationFields?.fields?.checkout?.driver_tip?.required && (Number(cart?.driver_tip) <= 0))} disabled={loading || !cart?.valid || !paymethodSelected || placing || errorCash || cart?.subtotal_to_calculate < cart?.minimum || paymethodSelected?.gateway === 'paypal' || (options.type === 1 && validationFields?.fields?.checkout?.driver_tip?.enabled && validationFields?.fields?.checkout?.driver_tip?.required && (Number(cart?.driver_tip) <= 0))} btnText={cart?.subtotal_to_calculate >= cart?.minimum ? ( placing ? t('PLACING', 'Placing') : loading ? t('LOADING', 'Loading') : t('PLACE_ORDER', 'Place Order') ) : (`${t('MINIMUN_SUBTOTAL_ORDER', 'Minimum subtotal order:')} ${parsePrice(cart?.minimum)}`) } /> )} {webviewPaymethod?.gateway === 'paypal' && showGateway.open && ( )} {webviewPaymethod?.gateway === 'square' && showGateway.open && ( )} {openOrderCreating && ( )} ) } export const Checkout = (props: any) => { const { errors, clearErrors, cartUuid, stripePaymentOptions, onNavigationRedirect, navigation } = props const [, { showToast }] = useToast(); const [, t] = useLanguage(); const [{ token }] = useSession(); const [ordering] = useApi(); const [, { confirmCart }] = useOrder(); const { confirmPayment, loading: confirmPaymentLoading } = useConfirmPayment(); const [cartState, setCartState] = useState({ loading: true, error: [], cart: null }); const confirmMethods = ['stripe_redirect', 'paypal'] const getOrder = async (cartId: any) => { try { setCartState({ ...cartState, loading: true }) const url = `${ordering.root}/carts/${cartId}` const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` } }) const { result } = await response.json(); let publicKey = null try { const { content } = await ordering.setAccessToken(token).paymentCards().getCredentials(); if (!content.error) { publicKey = content.result.publishable; } } catch (error) { publicKey = null } if (result.status === 1 && result.order?.uuid) { onNavigationRedirect('OrderDetails', { orderId: result.order.uuid }) setCartState({ ...cartState, loading: false }) } else if (result.status === 2 && confirmMethods.includes(result.paymethod_data?.gateway)) { try { const confirmCartRes = await confirmCart(cartUuid) if (confirmCartRes.error) { showToast(ToastType.Error, confirmCartRes.error.message) } if (confirmCartRes.result.order?.uuid) { onNavigationRedirect('OrderDetails', { orderId: confirmCartRes.result.order.uuid, isFromCheckout: true }) } setCartState({ ...cartState, loading: false, cart: result }) } catch (error: any) { showToast(ToastType.Error, error?.toString() || error.message) } } else if (result.status === 2 && stripePaymentOptions.includes(result.paymethod_data?.gateway)) { const clientSecret = result.paymethod_data?.result?.client_secret const paymentMethodId = result.paymethod_data?.data?.source_id; initStripe({ publishableKey: publicKey }); try { const { paymentIntent, error } = await confirmPayment(clientSecret, { type: 'Card', paymentMethodId }); try { const confirmCartRes = await confirmCart(cartUuid) if (confirmCartRes.error) { showToast(ToastType.Error, confirmCartRes.error.message) setCartState({ ...cartState, loading: false, cart: result }) return } if (confirmCartRes.result.order?.uuid) { onNavigationRedirect('OrderDetails', { orderId: confirmCartRes.result.order.uuid, isFromCheckout: true }) setCartState({ ...cartState, loading: false }) } else { showToast(ToastType.Error, t('FAILED_PAYMENT', 'The payment has failed')); const cart = Array.isArray(result) ? null : result setCartState({ ...cartState, loading: false, cart, error: cart ? null : result }) return } } catch (error: any) { showToast(ToastType.Error, error?.toString() || error.message) } } catch (error) { showToast(ToastType.Error, t('FAILED_PAYMENT', 'The payment has failed')); const cart = Array.isArray(result) ? null : result setCartState({ ...cartState, loading: false, cart, error: cart ? null : result }) } } else { const cart = Array.isArray(result) ? null : result setCartState({ ...cartState, loading: false, cart, error: cart ? null : result }) } } catch (e: any) { setCartState({ ...cartState, loading: false, error: [e.toString()] }) } } useEffect(() => { if (errors) { const errorText = manageErrorsToShow(errors) showToast(ToastType.Error, errorText) clearErrors && clearErrors() } }, [errors]) useEffect(() => { if (token && cartUuid) { getOrder(cartUuid) } }, [token, cartUuid]) const checkoutProps = { ...props, UIComponent: CheckoutUI, cartState, businessId: cartState.cart?.business_id } return ( <> {cartState?.error?.length > 0 ? ( navigation.navigate('BusinessList')} /> ) : ( )} ) }