diff --git a/frontend/src/assets/css/common.css b/frontend/src/assets/css/common.css index 1311fa20c..36906d21b 100644 --- a/frontend/src/assets/css/common.css +++ b/frontend/src/assets/css/common.css @@ -226,7 +226,7 @@ div.content { @media only screen and (width <=550px) { .search-dialog-content { - width: calc(100% - 20px); + width: auto; max-width: 480px; } } diff --git a/frontend/src/config/env.config.ts b/frontend/src/config/env.config.ts index 381d44e7d..471cf8958 100644 --- a/frontend/src/config/env.config.ts +++ b/frontend/src/config/env.config.ts @@ -48,10 +48,6 @@ const CURRENCIES: Currency[] = [ code: 'GBP', symbol: '£', } - // { - // code: 'IQD', - // symbol: 'IQD', - // } ] const env = { diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 31f8c50c6..417121ec5 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -546,7 +546,7 @@ const Home = () => { { @@ -557,15 +557,15 @@ const Home = () => { { - setOpenLocationSearchFormDialog(false) - }} + // onCancel={() => { + // setOpenLocationSearchFormDialog(false) + // }} /> { diff --git a/mobile/.env.example b/mobile/.env.example index 90b8f3175..d3e62aaec 100644 --- a/mobile/.env.example +++ b/mobile/.env.example @@ -15,8 +15,7 @@ BC_MINIMUM_AGE=21 BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY BC_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER BC_STRIPE_COUNTRY_CODE=US -BC_STRIPE_CURRENCY_CODE=USD -BC_CURRENCY=$ +BC_BASE_CURRENCY=USD BC_DEPOSIT_FILTER_VALUE_1=250 BC_DEPOSIT_FILTER_VALUE_2=500 BC_DEPOSIT_FILTER_VALUE_3=750 diff --git a/mobile/common/helper.ts b/mobile/common/helper.ts index 7e10f9685..d4dbd0027 100644 --- a/mobile/common/helper.ts +++ b/mobile/common/helper.ts @@ -3,14 +3,16 @@ import * as Device from 'expo-device' import * as Notifications from 'expo-notifications' import Constants from 'expo-constants' import type { NativeStackNavigationProp } from '@react-navigation/native-stack' -import { CommonActions, DrawerActions, RouteProp } from '@react-navigation/native' - +import { CommonActions, DrawerActions, NavigationRoute, RouteProp } from '@react-navigation/native' import mime from 'mime' + import i18n from '@/lang/i18n' import * as UserService from '@/services/UserService' +import * as StripeService from '@/services/StripeService' import * as bookcarsTypes from ':bookcars-types' import * as bookcarsHelper from ':bookcars-helper' -import * as toastHelper from './toastHelper' +import * as toastHelper from '@/common/toastHelper' +import * as env from '@/config/env.config' /** * Indicate whether Platform OS is Android or not. @@ -240,7 +242,7 @@ export const getFuelPolicy = (type: string) => { * @param {string} language * @returns {string} */ -export const getCancellation = (cancellation: number, language: string) => { +export const getCancellation = async (cancellation: number, language: string) => { const fr = bookcarsHelper.isFrench(language) if (cancellation === -1) { @@ -248,7 +250,8 @@ export const getCancellation = (cancellation: number, language: string) => { } if (cancellation === 0) { return `${i18n.t('CANCELLATION')}${fr ? ' : ' : ': '}${i18n.t('INCLUDED')}${fr ? 'e' : ''}` } - return `${i18n.t('CANCELLATION')}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(cancellation, i18n.t('CURRENCY'), language)}` + const _cancellation = await StripeService.convertPrice(cancellation) + return `${i18n.t('CANCELLATION')}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(_cancellation, (await StripeService.getCurrencySymbol()), language)}` } /** @@ -258,7 +261,7 @@ export const getCancellation = (cancellation: number, language: string) => { * @param {string} language * @returns {string} */ -export const getAmendments = (amendments: number, language: string) => { +export const getAmendments = async (amendments: number, language: string) => { const fr = bookcarsHelper.isFrench(language) if (amendments === -1) { @@ -266,7 +269,8 @@ export const getAmendments = (amendments: number, language: string) => { } if (amendments === 0) { return `${i18n.t('AMENDMENTS')}${fr ? ' : ' : ': '}${i18n.t('INCLUDED')}${fr ? 'es' : ''}` } - return `${i18n.t('AMENDMENTS')}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(amendments, i18n.t('CURRENCY'), language)}` + const _amendments = await StripeService.convertPrice(amendments) + return `${i18n.t('AMENDMENTS')}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(_amendments, (await StripeService.getCurrencySymbol()), language)}` } /** @@ -276,7 +280,7 @@ export const getAmendments = (amendments: number, language: string) => { * @param {string} language * @returns {string} */ -export const getTheftProtection = (theftProtection: number, language: string) => { +export const getTheftProtection = async (theftProtection: number, language: string) => { const fr = bookcarsHelper.isFrench(language) if (theftProtection === -1) { @@ -284,7 +288,8 @@ export const getTheftProtection = (theftProtection: number, language: string) => } if (theftProtection === 0) { return `${i18n.t('THEFT_PROTECTION')}${fr ? ' : ' : ': '}${i18n.t('INCLUDED')}${fr ? 'e' : ''}` } - return `${i18n.t('THEFT_PROTECTION')}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(theftProtection, i18n.t('CURRENCY'), language)}${i18n.t('DAILY')}` + const _theftProtection = await StripeService.convertPrice(theftProtection) + return `${i18n.t('THEFT_PROTECTION')}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(_theftProtection, (await StripeService.getCurrencySymbol()), language)}${i18n.t('DAILY')}` } /** @@ -294,7 +299,7 @@ export const getTheftProtection = (theftProtection: number, language: string) => * @param {string} language * @returns {string} */ -export const getCollisionDamageWaiver = (collisionDamageWaiver: number, language: string) => { +export const getCollisionDamageWaiver = async (collisionDamageWaiver: number, language: string) => { const fr = bookcarsHelper.isFrench(language) if (collisionDamageWaiver === -1) { @@ -302,7 +307,8 @@ export const getCollisionDamageWaiver = (collisionDamageWaiver: number, language } if (collisionDamageWaiver === 0) { return `${i18n.t('COLLISION_DAMAGE_WAVER')}${fr ? ' : ' : ': '}${i18n.t('INCLUDED')}${fr ? 'e' : ''}` } - return `${i18n.t('COLLISION_DAMAGE_WAVER')}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(collisionDamageWaiver, i18n.t('CURRENCY'), language)}${i18n.t('DAILY')}` + const _collisionDamageWaiver = await StripeService.convertPrice(collisionDamageWaiver) + return `${i18n.t('COLLISION_DAMAGE_WAVER')}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(_collisionDamageWaiver, (await StripeService.getCurrencySymbol()), language)}${i18n.t('DAILY')}` } /** @@ -312,7 +318,7 @@ export const getCollisionDamageWaiver = (collisionDamageWaiver: number, language * @param {string} language * @returns {string} */ -export const getFullInsurance = (fullInsurance: number, language: string) => { +export const getFullInsurance = async (fullInsurance: number, language: string) => { const fr = bookcarsHelper.isFrench(language) if (fullInsurance === -1) { @@ -320,7 +326,8 @@ export const getFullInsurance = (fullInsurance: number, language: string) => { } if (fullInsurance === 0) { return `${i18n.t('FULL_INSURANCE')}${fr ? ' : ' : ': '}${i18n.t('INCLUDED')}${fr ? 'e' : ''}` } - return `${i18n.t('FULL_INSURANCE')}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(fullInsurance, i18n.t('CURRENCY'), language)}${i18n.t('DAILY')}` + const _fullInsurance = await StripeService.convertPrice(fullInsurance) + return `${i18n.t('FULL_INSURANCE')}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(_fullInsurance, (await StripeService.getCurrencySymbol()), language)}${i18n.t('DAILY')}` } /** @@ -330,7 +337,7 @@ export const getFullInsurance = (fullInsurance: number, language: string) => { * @param {string} language * @returns {string} */ -export const getAdditionalDriver = (additionalDriver: number, language: string) => { +export const getAdditionalDriver = async (additionalDriver: number, language: string) => { const fr = bookcarsHelper.isFrench(language) if (additionalDriver === -1) { @@ -338,7 +345,8 @@ export const getAdditionalDriver = (additionalDriver: number, language: string) } if (additionalDriver === 0) { return `${i18n.t('ADDITIONAL_DRIVER')}${fr ? ' : ' : ': '}${i18n.t('INCLUDED')}` } - return `${i18n.t('ADDITIONAL_DRIVER')}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(additionalDriver, i18n.t('CURRENCY'), language)}${i18n.t('DAILY')}` + const _additionalDriver = await StripeService.convertPrice(additionalDriver) + return `${i18n.t('ADDITIONAL_DRIVER')}${fr ? ' : ' : ': '}${bookcarsHelper.formatPrice(_additionalDriver, (await StripeService.getCurrencySymbol()), language)}${i18n.t('DAILY')}` } /** @@ -365,7 +373,7 @@ export const getDaysShort = (days: number) => `${days} ${i18n.t('PRICE_DAYS_PART * @param {?boolean} [hidePlus] * @returns {string} */ -export const getCancellationOption = (cancellation: number, language: string, hidePlus?: boolean) => { +export const getCancellationOption = async (cancellation: number, language: string, hidePlus?: boolean) => { const fr = bookcarsHelper.isFrench(language) if (cancellation === -1) { @@ -373,7 +381,8 @@ export const getCancellationOption = (cancellation: number, language: string, hi } if (cancellation === 0) { return `${i18n.t('INCLUDED')}${fr ? 'e' : ''}` } - return `${hidePlus ? '' : '+ '}${bookcarsHelper.formatPrice(cancellation, i18n.t('CURRENCY'), language)}` + const _cancellation = await StripeService.convertPrice(cancellation) + return `${hidePlus ? '' : '+ '}${bookcarsHelper.formatPrice(_cancellation, (await StripeService.getCurrencySymbol()), language)}` } /** @@ -384,7 +393,7 @@ export const getCancellationOption = (cancellation: number, language: string, hi * @param {?boolean} [hidePlus] * @returns {string} */ -export const getAmendmentsOption = (amendments: number, language: string, hidePlus?: boolean) => { +export const getAmendmentsOption = async (amendments: number, language: string, hidePlus?: boolean) => { const fr = bookcarsHelper.isFrench(language) if (amendments === -1) { @@ -392,7 +401,8 @@ export const getAmendmentsOption = (amendments: number, language: string, hidePl } if (amendments === 0) { return `${i18n.t('INCLUDED')}${fr ? 'es' : ''}` } - return `${hidePlus ? '' : '+ '}${bookcarsHelper.formatPrice(amendments, i18n.t('CURRENCY'), language)}` + const _amendments = await StripeService.convertPrice(amendments) + return `${hidePlus ? '' : '+ '}${bookcarsHelper.formatPrice(_amendments, (await StripeService.getCurrencySymbol()), language)}` } /** @@ -404,7 +414,7 @@ export const getAmendmentsOption = (amendments: number, language: string, hidePl * @param {?boolean} [hidePlus] * @returns {string} */ -export const getCollisionDamageWaiverOption = (collisionDamageWaiver: number, days: number, language: string, hidePlus?: boolean) => { +export const getCollisionDamageWaiverOption = async (collisionDamageWaiver: number, days: number, language: string, hidePlus?: boolean) => { const fr = bookcarsHelper.isFrench(language) if (collisionDamageWaiver === -1) { @@ -412,7 +422,8 @@ export const getCollisionDamageWaiverOption = (collisionDamageWaiver: number, da } if (collisionDamageWaiver === 0) { return `${i18n.t('INCLUDED')}${fr ? 'e' : ''}` } - return `${hidePlus ? '' : '+ '}${bookcarsHelper.formatPrice(collisionDamageWaiver * days, i18n.t('CURRENCY'), language)} (${bookcarsHelper.formatPrice(collisionDamageWaiver, i18n.t('CURRENCY'), language)}${i18n.t('DAILY')})` + const _collisionDamageWaiver = await StripeService.convertPrice(collisionDamageWaiver) + return `${hidePlus ? '' : '+ '}${bookcarsHelper.formatPrice(_collisionDamageWaiver * days, (await StripeService.getCurrencySymbol()), language)} (${bookcarsHelper.formatPrice(_collisionDamageWaiver, (await StripeService.getCurrencySymbol()), language)}${i18n.t('DAILY')})` } /** @@ -424,7 +435,7 @@ export const getCollisionDamageWaiverOption = (collisionDamageWaiver: number, da * @param {?boolean} [hidePlus] * @returns {string} */ -export const getTheftProtectionOption = (theftProtection: number, days: number, language: string, hidePlus?: boolean) => { +export const getTheftProtectionOption = async (theftProtection: number, days: number, language: string, hidePlus?: boolean) => { const fr = bookcarsHelper.isFrench(language) if (theftProtection === -1) { @@ -432,7 +443,8 @@ export const getTheftProtectionOption = (theftProtection: number, days: number, } if (theftProtection === 0) { return `${i18n.t('INCLUDED')}${fr ? 'e' : ''}` } - return `${hidePlus ? '' : '+ '}${bookcarsHelper.formatPrice(theftProtection * days, i18n.t('CURRENCY'), language)} (${bookcarsHelper.formatPrice(theftProtection, i18n.t('CURRENCY'), language)}${i18n.t('DAILY')})` + const _theftProtection = await StripeService.convertPrice(theftProtection) + return `${hidePlus ? '' : '+ '}${bookcarsHelper.formatPrice(_theftProtection * days, (await StripeService.getCurrencySymbol()), language)} (${bookcarsHelper.formatPrice(_theftProtection, (await StripeService.getCurrencySymbol()), language)}${i18n.t('DAILY')})` } /** @@ -444,7 +456,7 @@ export const getTheftProtectionOption = (theftProtection: number, days: number, * @param {?boolean} [hidePlus] * @returns {string} */ -export const getFullInsuranceOption = (fullInsurance: number, days: number, language: string, hidePlus?: boolean) => { +export const getFullInsuranceOption = async (fullInsurance: number, days: number, language: string, hidePlus?: boolean) => { const fr = bookcarsHelper.isFrench(language) if (fullInsurance === -1) { @@ -452,7 +464,8 @@ export const getFullInsuranceOption = (fullInsurance: number, days: number, lang } if (fullInsurance === 0) { return `${i18n.t('INCLUDED')}${fr ? 'e' : ''}` } - return `${hidePlus ? '' : '+ '}${bookcarsHelper.formatPrice(fullInsurance * days, i18n.t('CURRENCY'), language)} (${bookcarsHelper.formatPrice(fullInsurance, i18n.t('CURRENCY'), language)}${i18n.t('DAILY')})` + const _fullInsurance = await StripeService.convertPrice(fullInsurance) + return `${hidePlus ? '' : '+ '}${bookcarsHelper.formatPrice(_fullInsurance * days, (await StripeService.getCurrencySymbol()), language)} (${bookcarsHelper.formatPrice(_fullInsurance, (await StripeService.getCurrencySymbol()), language)}${i18n.t('DAILY')})` } /** @@ -464,13 +477,14 @@ export const getFullInsuranceOption = (fullInsurance: number, days: number, lang * @param {?boolean} [hidePlus] * @returns {string} */ -export const getAdditionalDriverOption = (additionalDriver: number, days: number, language: string, hidePlus?: boolean) => { +export const getAdditionalDriverOption = async (additionalDriver: number, days: number, language: string, hidePlus?: boolean) => { if (additionalDriver === -1) { return i18n.t('UNAVAILABLE') } if (additionalDriver === 0) { return i18n.t('INCLUDED') } - return `${hidePlus ? '' : '+ '}${bookcarsHelper.formatPrice(additionalDriver * days, i18n.t('CURRENCY'), language)} (${bookcarsHelper.formatPrice(additionalDriver, i18n.t('CURRENCY'), language)}${i18n.t('DAILY')})` + const _additionalDriver = await StripeService.convertPrice(additionalDriver) + return `${hidePlus ? '' : '+ '}${bookcarsHelper.formatPrice(_additionalDriver * days, (await StripeService.getCurrencySymbol()), language)} (${bookcarsHelper.formatPrice(_additionalDriver, (await StripeService.getCurrencySymbol()), language)}${i18n.t('DAILY')})` } /** @@ -573,18 +587,25 @@ export const navigate = ( navigation.dispatch((state) => { const { routes } = state const index = routes.findIndex((r) => r.name === route.name) - routes.splice(index, 1) + const _routes = bookcarsHelper.cloneArray(routes) as NavigationRoute[] + // _routes.splice(index, 1) const now = Date.now() - routes.push({ + _routes[index] = { name: route.name, key: `${route.name}-${now}`, params, - }) + } + // _routes.push({ + // name: route.name, + // key: `${route.name}-${now}`, + // params, + // }) return CommonActions.reset({ ...state, - routes, - index: routes.length - 1, + routes: _routes, + // index: routes.length - 1, + index, }) }) navigation.dispatch(DrawerActions.closeDrawer()) @@ -602,18 +623,26 @@ export const navigate = ( navigation.dispatch((state) => { const { routes } = state const index = routes.findIndex((r) => r.name === 'Booking') - routes.splice(index, 1) + const _routes = bookcarsHelper.cloneArray(routes) as NavigationRoute[] + // _routes.splice(index, 1) + // const now = Date.now() + // _routes.push({ + // name: 'Booking', + // key: `Booking-${now}`, + // params, + // }) const now = Date.now() - routes.push({ - name: 'Booking', - key: `Booking-${now}`, + _routes[index] = { + name: route.name, + key: `${route.name}-${now}`, params, - }) + } return CommonActions.reset({ ...state, - routes, - index: routes.length - 1, + routes: _routes, + // index: routes.length - 1, + index, }) }) navigation.dispatch(DrawerActions.closeDrawer()) @@ -637,26 +666,31 @@ export const navigate = ( navigation.dispatch((state) => { const { routes } = state const index = routes.findIndex((r) => r.name === 'Cars') - routes.splice(index, 1) + const _routes = bookcarsHelper.cloneArray(routes) as NavigationRoute[] + // _routes.splice(index, 1) + // const now = Date.now() + // _routes.push({ + // name: 'Cars', + // key: `Cars-${now}`, + // params, + // }) const now = Date.now() - routes.push({ - name: 'Cars', - key: `Cars-${now}`, + _routes[index] = { + name: route.name, + key: `${route.name}-${now}`, params, - }) + } return CommonActions.reset({ ...state, - routes, - index: routes.length - 1, + routes: _routes, + // index: routes.length - 1, + index, }) }) navigation.dispatch(DrawerActions.closeDrawer()) } else { - navigation.navigate( - route.name, - params, - ) + navigation.navigate(route.name, params) } break } @@ -673,18 +707,26 @@ export const navigate = ( navigation.dispatch((state) => { const { routes } = state const index = routes.findIndex((r) => r.name === 'Checkout') - routes.splice(index, 1) + const _routes = bookcarsHelper.cloneArray(routes) as NavigationRoute[] + // _routes.splice(index, 1) + // const now = Date.now() + // _routes.push({ + // name: 'Checkout', + // key: `Checkout-${now}`, + // params, + // }) const now = Date.now() - routes.push({ - name: 'Checkout', - key: `Checkout-${now}`, + _routes[index] = { + name: route.name, + key: `${route.name}-${now}`, params, - }) + } return CommonActions.reset({ ...state, - routes, - index: routes.length - 1, + routes: _routes, + // index: routes.length - 1, + index, }) }) navigation.dispatch(DrawerActions.closeDrawer()) @@ -700,3 +742,30 @@ export const navigate = ( break } } + +type DepositFilterValue = 'value1' | 'value2' | 'value3' + +export const getDepositFilterValue = async (language: string, value: DepositFilterValue): Promise => { + const currency = await StripeService.getCurrencySymbol() + const isCurrencyRTL = await StripeService.currencyRTL() + + let depositFilterValue = 0 + + if (value === 'value1') { + depositFilterValue = await StripeService.convertPrice(env.DEPOSIT_FILTER_VALUE_1) + } else if (value === 'value2') { + depositFilterValue = await StripeService.convertPrice(env.DEPOSIT_FILTER_VALUE_2) + } else if (value === 'value3') { + depositFilterValue = await StripeService.convertPrice(env.DEPOSIT_FILTER_VALUE_3) + } + + switch (language) { + case 'fr': + return `Moins de ${isCurrencyRTL ? currency : ''}${depositFilterValue}${!isCurrencyRTL ? (` ${currency}`) : ''}` + case 'es': + return `Menos de ${isCurrencyRTL ? currency : ''}${depositFilterValue}${!isCurrencyRTL ? (` ${currency}`) : ''}` + case 'en': + default: + return `Less than ${isCurrencyRTL ? currency : ''}${depositFilterValue}${!isCurrencyRTL ? (` ${currency}`) : ''}` + } +} diff --git a/mobile/components/Booking.tsx b/mobile/components/Booking.tsx index cc63e79d7..2e71955b0 100644 --- a/mobile/components/Booking.tsx +++ b/mobile/components/Booking.tsx @@ -1,4 +1,4 @@ -import React, { memo } from 'react' +import React, { memo, useEffect, useState } from 'react' import { StyleSheet, Text, View, Image } from 'react-native' import { MaterialIcons } from '@expo/vector-icons' import { Locale, format } from 'date-fns' @@ -10,6 +10,7 @@ import Button from './Button' import * as helper from '@/common/helper' import * as env from '@/config/env.config' import i18n from '@/lang/i18n' +import * as StripeService from '@/services/StripeService' interface BookingProps { booking: bookcarsTypes.Booking @@ -44,7 +45,35 @@ const Booking = ({ const _fr = bookcarsHelper.isFrench(language) const _format = _fr ? 'eee d LLL yyyy kk:mm' : 'eee, d LLL yyyy, p' - return ( + const [loading, setLoading] = useState(true) + const [currencySymbol, setCurrencySymbol] = useState('') + const [price, setPrice] = useState(0) + const [cancellation, setCancellation] = useState('') + const [amendments, setAmendments] = useState('') + const [collisionDamageWaiver, setCollisionDamageWaiver] = useState('') + const [theftProtection, setTheftProtection] = useState('') + const [fullInsurance, setFullInsurance] = useState('') + const [additionalDriver, setAdditionalDriver] = useState('') + + useEffect(() => { + const init = async () => { + if (booking && car && language && days) { + setCurrencySymbol(await StripeService.getCurrencySymbol()) + setPrice(await StripeService.convertPrice(booking.price!)) + setCancellation(await helper.getCancellationOption(car.cancellation, language, true)) + setAmendments(await helper.getAmendmentsOption(car.amendments, language, true)) + setCollisionDamageWaiver(await helper.getCollisionDamageWaiverOption(car.collisionDamageWaiver, days, language, true)) + setTheftProtection(await helper.getTheftProtectionOption(car.theftProtection, days, language, true)) + setFullInsurance(await helper.getFullInsuranceOption(car.fullInsurance, days, language, true)) + setAdditionalDriver(await helper.getAdditionalDriverOption(car.additionalDriver, days, language, true)) + setLoading(false) + } + } + + init() + }, [booking, car.additionalDriver, car.amendments, car.cancellation, car.collisionDamageWaiver, car.fullInsurance, car.theftProtection, days, language]) + + return !loading && price && ( @@ -89,7 +118,7 @@ const Booking = ({ {i18n.t('CANCELLATION')} - {helper.getCancellationOption(car.cancellation, language, true)} + {cancellation} )} @@ -97,7 +126,7 @@ const Booking = ({ {i18n.t('AMENDMENTS')} - {helper.getAmendmentsOption(car.amendments, language, true)} + {amendments} )} @@ -105,7 +134,7 @@ const Booking = ({ {i18n.t('COLLISION_DAMAGE_WAVER')} - {helper.getCollisionDamageWaiverOption(car.collisionDamageWaiver, days, language, true)} + {collisionDamageWaiver} )} @@ -113,7 +142,7 @@ const Booking = ({ {i18n.t('THEFT_PROTECTION')} - {helper.getTheftProtectionOption(car.theftProtection, days, language, true)} + {theftProtection} )} @@ -121,7 +150,7 @@ const Booking = ({ {i18n.t('FULL_INSURANCE')} - {helper.getFullInsuranceOption(car.fullInsurance, days, language, true)} + {fullInsurance} )} @@ -129,7 +158,7 @@ const Booking = ({ {i18n.t('ADDITIONAL_DRIVER')} - {helper.getAdditionalDriverOption(car.additionalDriver, days, language, true)} + {additionalDriver} )} @@ -137,7 +166,7 @@ const Booking = ({ )} {i18n.t('COST')} - {`${bookcarsHelper.formatPrice(booking.price as number, i18n.t('CURRENCY'), language)}`} + {`${bookcarsHelper.formatPrice(price, currencySymbol, language)}`} {booking.cancellation && !booking.cancelRequest diff --git a/mobile/components/Car.tsx b/mobile/components/Car.tsx index e46811a08..0642581e7 100644 --- a/mobile/components/Car.tsx +++ b/mobile/components/Car.tsx @@ -10,6 +10,7 @@ import Button from './Button' import * as helper from '@/common/helper' import * as env from '@/config/env.config' import i18n from '@/lang/i18n' +import * as StripeService from '@/services/StripeService' interface CarProps { navigation: NativeStackNavigationProp @@ -46,14 +47,34 @@ const Car = ({ const fr = bookcarsHelper.isFrench(language) const [days, setDays] = useState() + const [loading, setLoading] = useState(true) + const [currencySymbol, setCurrencySymbol] = useState('') const [totalPrice, setTotalPrice] = useState() + const [cancellation, setCancellation] = useState('') + const [amendments, setAmendments] = useState('') + const [collisionDamageWaiver, setCollisionDamageWaiver] = useState('') + const [theftProtection, setTheftProtection] = useState('') + const [fullInsurance, setFullInsurance] = useState('') + const [additionalDriver, setAdditionalDriver] = useState('') useEffect(() => { - if (car && from && to) { - setDays(bookcarsHelper.days(from, to)) - setTotalPrice(bookcarsHelper.calculateTotalPrice(car, from as Date, to as Date)) + const init = async () => { + if (car && from && to && language) { + setCurrencySymbol(await StripeService.getCurrencySymbol()) + setDays(bookcarsHelper.days(from, to)) + setTotalPrice(await StripeService.convertPrice(bookcarsHelper.calculateTotalPrice(car, from as Date, to as Date))) + setCancellation(await helper.getCancellation(car.cancellation, language)) + setAmendments(await helper.getAmendments(car.amendments, language)) + setCollisionDamageWaiver(await helper.getCollisionDamageWaiver(car.collisionDamageWaiver, language)) + setTheftProtection(await helper.getTheftProtection(car.theftProtection, language)) + setFullInsurance(await helper.getFullInsurance(car.fullInsurance, language)) + setAdditionalDriver(await helper.getAdditionalDriver(car.additionalDriver, language)) + setLoading(false) + } } - }, [car, from, to]) + + init() + }, [car, from, language, to]) const styles = StyleSheet.create({ carContainer: { @@ -248,7 +269,7 @@ const Car = ({ }, }) - return days && totalPrice && ( + return !loading && days && totalPrice && ( {pickupLocationName && ( <> @@ -318,37 +339,37 @@ const Car = ({ {car.cancellation > -1 && ( - {helper.getCancellation(car.cancellation, language)} + {cancellation} )} {car.amendments > -1 && ( - {helper.getAmendments(car.amendments, language)} + {amendments} )} {car.theftProtection > -1 && ( - {helper.getTheftProtection(car.theftProtection, language)} + {theftProtection} )} {car.collisionDamageWaiver > -1 && ( - {helper.getCollisionDamageWaiver(car.collisionDamageWaiver, language)} + {collisionDamageWaiver} )} {car.fullInsurance > -1 && ( - {helper.getFullInsurance(car.fullInsurance, language)} + {fullInsurance} )} {car.additionalDriver > -1 && ( - {helper.getAdditionalDriver(car.additionalDriver, language)} + {additionalDriver} )} @@ -395,8 +416,8 @@ const Car = ({ {!hidePrice && from && to && ( {helper.getDays(days)} - {`${bookcarsHelper.formatPrice(totalPrice, i18n.t('CURRENCY'), language)}`} - {`${i18n.t('PRICE_PER_DAY')} ${bookcarsHelper.formatPrice(totalPrice / days, i18n.t('CURRENCY'), language)}`} + {`${bookcarsHelper.formatPrice(totalPrice, currencySymbol, language)}`} + {`${i18n.t('PRICE_PER_DAY')} ${bookcarsHelper.formatPrice(totalPrice / days, currencySymbol, language)}`} )} diff --git a/mobile/components/CarList.tsx b/mobile/components/CarList.tsx index c92606c30..e282ac589 100644 --- a/mobile/components/CarList.tsx +++ b/mobile/components/CarList.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react' import { StyleSheet, Text, View, ActivityIndicator, RefreshControl } from 'react-native' import { KeyboardAwareFlatList } from 'react-native-keyboard-aware-scroll-view' import type { NativeStackNavigationProp } from '@react-navigation/native-stack' +import { RouteProp } from '@react-navigation/native' import * as bookcarsTypes from ':bookcars-types' import * as helper from '@/common/helper' @@ -34,7 +35,8 @@ interface CarListProps { cars?: bookcarsTypes.Car[] hidePrice?: boolean footerComponent?: React.ReactElement - route?: 'Cars' | 'Checkout', + routeName?: 'Cars' | 'Checkout', + route: RouteProp onLoad?: bookcarsTypes.DataEvent } @@ -61,6 +63,7 @@ const CarList = ({ cars, hidePrice, footerComponent, + routeName, route, onLoad }: CarListProps) => { @@ -247,25 +250,27 @@ const CarList = ({ { setRefreshing(true) - if ((route && pickupLocation && dropOffLocation && from && to) && ((route === 'Checkout' && cars && cars.length > 0) || route === 'Cars')) { - if (route === 'Cars') { - navigation.navigate(route, { - pickupLocation: pickupLocation!, - dropOffLocation: dropOffLocation!, - from: from!.getTime(), - to: to!.getTime(), - d: Date.now(), - }) - } else { - navigation.navigate(route, { - car: cars![0]._id, - pickupLocation: pickupLocation!, - dropOffLocation: dropOffLocation!, - from: from!.getTime(), - to: to!.getTime(), - d: Date.now(), - }) - } + if ((routeName && pickupLocation && dropOffLocation && from && to) && ((routeName === 'Checkout' && cars && cars.length > 0) || routeName === 'Cars')) { + helper.navigate(route, navigation, true) + + // if (route === 'Cars') { + // navigation.navigate(route, { + // pickupLocation: pickupLocation!, + // dropOffLocation: dropOffLocation!, + // from: from!.getTime(), + // to: to!.getTime(), + // d: Date.now(), + // }) + // } else { + // navigation.navigate(route, { + // car: cars![0]._id, + // pickupLocation: pickupLocation!, + // dropOffLocation: dropOffLocation!, + // from: from!.getTime(), + // to: to!.getTime(), + // d: Date.now(), + // }) + // } // navigation.dispatch((state) => { // const { routes } = state diff --git a/mobile/components/CurrencyMenu.tsx b/mobile/components/CurrencyMenu.tsx new file mode 100644 index 000000000..0c2ac22cb --- /dev/null +++ b/mobile/components/CurrencyMenu.tsx @@ -0,0 +1,100 @@ +import React, { useEffect, useState } from 'react' +import { Pressable, StyleSheet, Text, View } from 'react-native' +import { RouteProp, useIsFocused, useNavigation } from '@react-navigation/native' +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import * as StripeService from '@/services/StripeService' +import { CURRENCIES } from '@/config/env.config' +import * as helper from '@/common/helper' + +interface CurrencyMenuProps { + route: RouteProp + textColor?: string + style?: object +} + +const CurrencyMenu = ({ + route, + textColor, + style, +}: CurrencyMenuProps) => { + const isFocused = useIsFocused() + const navigation = useNavigation>() + + const [value, setValue] = useState('') + const [showMenu, setShowMenu] = useState(false) + + useEffect(() => { + const init = async () => { + setValue(await StripeService.getCurrency()) + setShowMenu(false) + } + + init() + }, [route.name, isFocused]) + + const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + text: { + color: textColor || '#000', + fontWeight: '500' + }, + menu: { + position: 'absolute', + top: 42, + left: -14, + }, + menuItem: { + width: 70, + height: 32, + backgroundColor: '#fff', + color: '#515151', + // backgroundColor: '#feeee4', + // color: '#f37022', + textAlign: 'center', + verticalAlign: 'middle', + borderColor: '#f1f1f1', + borderWidth: 1, + }, + selected: { + backgroundColor: '#feeee4', + color: '#f37022', + } + }) + + return ( + <> + + setShowMenu((prev) => !prev)} + > + {value} + + + {showMenu && ( + + { + CURRENCIES.map((currency) => ( + { + await StripeService.setCurrency(currency.code) + helper.navigate(route, navigation, true) + setValue(currency.code) + setShowMenu(false) + }} + > + {currency.code} + + )) + } + + )} + + ) +} + +export default CurrencyMenu diff --git a/mobile/components/DepositFilter.tsx b/mobile/components/DepositFilter.tsx index fa402a0d6..30a861be8 100644 --- a/mobile/components/DepositFilter.tsx +++ b/mobile/components/DepositFilter.tsx @@ -1,18 +1,21 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { StyleSheet, View } from 'react-native' import * as env from '@/config/env.config' import i18n from '@/lang/i18n' import Accordion from './Accordion' import RadioButton from './RadioButton' +import * as helper from '@/common/helper' interface DepositFilterProps { + language: string visible?: boolean style?: object onChange?: (value: number) => void } const DepositFilter = ({ + language, visible, style, onChange @@ -21,6 +24,21 @@ const DepositFilter = ({ const [depositValue2, setDepositValue2] = useState(false) const [depositValue3, setDepositValue3] = useState(false) const [depositall, setDepositall] = useState(true) + const [depositValue1Text, setDepositValue1Text] = useState('') + const [depositValue2Text, setDepositValue2Text] = useState('') + const [depositValue3Text, setDepositValue3Text] = useState('') + const [loading, setLoading] = useState(true) + + useEffect(() => { + const init = async () => { + setDepositValue1Text(await helper.getDepositFilterValue(language, 'value1')) + setDepositValue2Text(await helper.getDepositFilterValue(language, 'value2')) + setDepositValue3Text(await helper.getDepositFilterValue(language, 'value3')) + setLoading(false) + } + + init() + }, [language]) const handleChange = (value: number) => { if (onChange) { @@ -76,13 +94,13 @@ const DepositFilter = ({ } return ( - visible && ( + !loading && visible && ( - - - + + + diff --git a/mobile/components/DrawerContent.tsx b/mobile/components/DrawerContent.tsx index 2a1f5db7c..4ca665ca6 100644 --- a/mobile/components/DrawerContent.tsx +++ b/mobile/components/DrawerContent.tsx @@ -66,7 +66,7 @@ const DrawerContent = ({ await UserService.setLanguage(__language) setLanguage(__language) const route = props.state.routes[index] - helper.navigate(route as RouteProp, navigation, false) + helper.navigate(route as RouteProp, navigation, true) } const currentUser = await UserService.getCurrentUser() diff --git a/mobile/components/Header.tsx b/mobile/components/Header.tsx index 4789a4661..78b62abba 100644 --- a/mobile/components/Header.tsx +++ b/mobile/components/Header.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' import { View, Text, StyleSheet, Pressable } from 'react-native' import { MaterialIcons } from '@expo/vector-icons' -import { useNavigation, DrawerActions } from '@react-navigation/native' +import { useNavigation, DrawerActions, RouteProp } from '@react-navigation/native' import { Avatar, Badge } from 'react-native-paper' import type { NativeStackNavigationProp } from '@react-navigation/native-stack' import * as bookcarsHelper from ':bookcars-helper' @@ -10,8 +10,10 @@ import * as UserService from '@/services/UserService' import * as env from '@/config/env.config' import { useGlobalContext, GlobalContextType } from '@/context/GlobalContext' import * as NotificationService from '@/services/NotificationService' +import CurrencyMenu from '@/components/CurrencyMenu' interface HeaderProps { + route?: RouteProp, title?: string hideTitle?: boolean loggedIn?: boolean @@ -19,7 +21,9 @@ interface HeaderProps { _avatar?: string | null } -const Header = ({ title, +const Header = ({ + route, + title, hideTitle, loggedIn, reload, @@ -56,7 +60,7 @@ const Header = ({ title, setAvatar(_avatar) }, [_avatar]) - return ( + return route && ( navigation.dispatch(DrawerActions.toggleDrawer())}> @@ -68,6 +72,12 @@ const Header = ({ title, )} {loggedIn && ( + + navigation.navigate('Notifications', {})}> {notificationCount > 0 && ( @@ -107,6 +117,9 @@ const styles = StyleSheet.create({ actions: { flexDirection: 'row', }, + currency: { + marginRight: 5, + }, notifications: { paddingTop: 5, paddingRight: 10, diff --git a/mobile/components/Layout.tsx b/mobile/components/Layout.tsx index 1f597167c..984fb2a9f 100644 --- a/mobile/components/Layout.tsx +++ b/mobile/components/Layout.tsx @@ -133,7 +133,7 @@ const Layout = ({ return ( -
+
{(!loading && ((!user && !strict) || (user && user.verified) ? ( {children} diff --git a/mobile/config/env.config.ts b/mobile/config/env.config.ts index 5782d8711..74d45af10 100644 --- a/mobile/config/env.config.ts +++ b/mobile/config/env.config.ts @@ -16,8 +16,7 @@ import { BC_STRIPE_PUBLISHABLE_KEY, BC_STRIPE_MERCHANT_IDENTIFIER, BC_STRIPE_COUNTRY_CODE, - BC_STRIPE_CURRENCY_CODE, - BC_CURRENCY, + BC_BASE_CURRENCY, BC_DEPOSIT_FILTER_VALUE_1, BC_DEPOSIT_FILTER_VALUE_2, BC_DEPOSIT_FILTER_VALUE_3, @@ -44,6 +43,29 @@ export const LANGUAGES = [ }, ] +type Currency = { code: string, symbol: string } + +/** + * The three-letter ISO 4217 alphabetic currency codes, e.g. "USD" or "EUR" and their symbols. + * https://docs.stripe.com/currencies + * + * @type {Currency[]} + */ +export const CURRENCIES: Currency[] = [ + { + code: 'USD', + symbol: '$', + }, + { + code: 'EUR', + symbol: '€', + }, + { + code: 'GBP', + symbol: '£', + } +] + /** * Application type. * @@ -199,19 +221,11 @@ export const STRIPE_MERCHANT_IDENTIFIER: string = BC_STRIPE_MERCHANT_IDENTIFIER export const STRIPE_COUNTRY_CODE: string = BC_STRIPE_COUNTRY_CODE /** - * The three-letter ISO 4217 alphabetic currency code, e.g. "USD" or "EUR". Required for Stripe payments. - * Must be a supported currency: https://docs.stripe.com/currencies - * - * @type {string} - */ -export const STRIPE_CURRENCY_CODE: string = BC_STRIPE_CURRENCY_CODE - -/** - * Currency. Default is $. + * The three-letter ISO 4217 alphabetic currency code, e.g. "USD" or "EUR" base currency. Default is USD. * * @type {string} */ -export const CURRENCY: string = BC_CURRENCY +export const BASE_CURRENCY: string = BC_BASE_CURRENCY || 'USD' /** * Deposit filter first value. @@ -233,10 +247,3 @@ export const DEPOSIT_FILTER_VALUE_2: number = Number(BC_DEPOSIT_FILTER_VALUE_2) * @type {number} */ export const DEPOSIT_FILTER_VALUE_3: number = Number(BC_DEPOSIT_FILTER_VALUE_3) - -/** - * Check if locale is US. - * - * @type {boolean} - */ -export const isUS = CURRENCY === '$' diff --git a/mobile/lang/en.ts b/mobile/lang/en.ts index f5df9f835..015a36dcb 100644 --- a/mobile/lang/en.ts +++ b/mobile/lang/en.ts @@ -112,7 +112,6 @@ export const en = { PASSWORD_UPDATE: 'Password changed successfully.', PASSWORD_UPDATE_ERROR: 'An error occurred while updating password.', EMPTY_CAR_LIST: 'No cars.', - CURRENCY: env.CURRENCY, DAILY: '/day', DIESEL_SHORT: 'D', GASOLINE_SHORT: 'G', @@ -224,9 +223,6 @@ export const en = { SEARCH_TITLE_2: ' for you', CAR_AVAILABLE: 'car available', CARS_AVAILABLE: 'cars available', - DEPOSIT_LESS_THAN_VALUE_1: `Less than ${env.isUS ? env.CURRENCY : ''}${env.DEPOSIT_FILTER_VALUE_1}${!env.isUS ? (` ${env.CURRENCY}`) : ''}`, - DEPOSIT_LESS_THAN_VALUE_2: `Less than ${env.isUS ? env.CURRENCY : ''}${env.DEPOSIT_FILTER_VALUE_2}${!env.isUS ? (` ${env.CURRENCY}`) : ''}`, - DEPOSIT_LESS_THAN_VALUE_3: `Less than ${env.isUS ? env.CURRENCY : ''}${env.DEPOSIT_FILTER_VALUE_3}${!env.isUS ? (` ${env.CURRENCY}`) : ''}`, CAR_SPECS: 'Car specs', AIRCON: 'Air Conditioning', MORE_THAN_FOOR_DOORS: '4+ doors', diff --git a/mobile/lang/es.ts b/mobile/lang/es.ts index eaac91eaf..c8ead5ae0 100644 --- a/mobile/lang/es.ts +++ b/mobile/lang/es.ts @@ -111,7 +111,6 @@ export const es = { PASSWORD_UPDATE: 'La contraseña ha sido modificada con éxito.', PASSWORD_UPDATE_ERROR: 'Se ha producido un error al cambiar la contraseña.', EMPTY_CAR_LIST: 'No hay coches.', - CURRENCY: env.CURRENCY, DAILY: '/día', DIESEL_SHORT: 'D', GASOLINE_SHORT: 'G', @@ -224,9 +223,6 @@ export const es = { SEARCH_TITLE_2: ' para ti', CAR_AVAILABLE: 'coche disponible', CARS_AVAILABLE: 'coches disponibles', - DEPOSIT_LESS_THAN_VALUE_1: `Menos de ${env.isUS ? env.CURRENCY : ''}${env.DEPOSIT_FILTER_VALUE_1}${!env.isUS ? (` ${env.CURRENCY}`) : ''}`, - DEPOSIT_LESS_THAN_VALUE_2: `Menos de ${env.isUS ? env.CURRENCY : ''}${env.DEPOSIT_FILTER_VALUE_2}${!env.isUS ? (` ${env.CURRENCY}`) : ''}`, - DEPOSIT_LESS_THAN_VALUE_3: `Menos de ${env.isUS ? env.CURRENCY : ''}${env.DEPOSIT_FILTER_VALUE_3}${!env.isUS ? (` ${env.CURRENCY}`) : ''}`, CAR_SPECS: 'Especificaciones del vehículo', AIRCON: 'Aire acondicionado', MORE_THAN_FOOR_DOORS: '4+ puertas', diff --git a/mobile/lang/fr.ts b/mobile/lang/fr.ts index 396c6d2fc..b80453e2a 100644 --- a/mobile/lang/fr.ts +++ b/mobile/lang/fr.ts @@ -111,7 +111,6 @@ export const fr = { PASSWORD_UPDATE: 'Le mot de passe a été mofifié avec succès.', PASSWORD_UPDATE_ERROR: "Une erreur s'est produite lors de la modification du mot de passe.", EMPTY_CAR_LIST: 'Pas de voitures.', - CURRENCY: env.CURRENCY, DAILY: '/jour', DIESEL_SHORT: 'D', GASOLINE_SHORT: 'E', @@ -224,9 +223,6 @@ export const fr = { SEARCH_TITLE_2: ' pour vous', CAR_AVAILABLE: 'voiture disponible', CARS_AVAILABLE: 'voitures disponibles', - DEPOSIT_LESS_THAN_VALUE_1: `Moins de ${env.isUS ? env.CURRENCY : ''}${env.DEPOSIT_FILTER_VALUE_1}${!env.isUS ? (` ${env.CURRENCY}`) : ''}`, - DEPOSIT_LESS_THAN_VALUE_2: `Moins de ${env.isUS ? env.CURRENCY : ''}${env.DEPOSIT_FILTER_VALUE_2}${!env.isUS ? (` ${env.CURRENCY}`) : ''}`, - DEPOSIT_LESS_THAN_VALUE_3: `Moins de ${env.isUS ? env.CURRENCY : ''}${env.DEPOSIT_FILTER_VALUE_3}${!env.isUS ? (` ${env.CURRENCY}`) : ''}`, CAR_SPECS: 'Spécificités du véhicule', AIRCON: 'Climatisation', MORE_THAN_FOOR_DOORS: '4+ portes', diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 7dfe25830..bbc47636e 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -8,6 +8,7 @@ "name": "bookcars", "version": "4.7.0", "dependencies": { + "@babel/runtime": "^7.26.0", "@react-native-async-storage/async-storage": "1.23.1", "@react-native-community/datetimepicker": "8.2.0", "@react-navigation/drawer": "^7.0.18", diff --git a/mobile/package.json b/mobile/package.json index aa8719fe0..131feb712 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -23,6 +23,7 @@ "prebuild:clean": "expo prebuild --clean" }, "dependencies": { + "@babel/runtime": "^7.26.0", "@react-native-async-storage/async-storage": "1.23.1", "@react-native-community/datetimepicker": "8.2.0", "@react-navigation/drawer": "^7.0.18", @@ -43,6 +44,7 @@ "expo-asset": "~11.0.1", "expo-constants": "~17.0.3", "expo-device": "~7.0.1", + "expo-document-picker": "~13.0.1", "expo-image-picker": "~16.0.3", "expo-linking": "^7.0.3", "expo-localization": "~16.0.0", @@ -69,8 +71,7 @@ "react-native-size-matters": "^0.4.2", "react-native-toast-message": "^2.2.1", "react-native-vector-icons": "^10.2.0", - "validator": "^13.12.0", - "expo-document-picker": "~13.0.1" + "validator": "^13.12.0" }, "devDependencies": { "@babel/core": "^7.26.0", diff --git a/mobile/screens/CarsScreen.tsx b/mobile/screens/CarsScreen.tsx index 5e0f8edb1..7ae438d01 100644 --- a/mobile/screens/CarsScreen.tsx +++ b/mobile/screens/CarsScreen.tsx @@ -28,6 +28,8 @@ import Indicator from '@/components/Indicator' const CarsScreen = ({ navigation, route }: NativeStackScreenProps) => { const isFocused = useIsFocused() + + const [language, setLanguage] = useState('') const [reload, setReload] = useState(false) const [loaded, setLoaded] = useState(false) const [visible, setVisible] = useState(false) @@ -73,8 +75,9 @@ const CarsScreen = ({ navigation, route }: NativeStackScreenProps { - const language = await UserService.getLanguage() - i18n.locale = language + const _language = await UserService.getLanguage() + i18n.locale = _language + setLanguage(_language) const _pickupLocation = await LocationService.getLocation(route.params.pickupLocation) setPickupLocation(_pickupLocation) @@ -173,11 +176,12 @@ const CarsScreen = ({ navigation, route }: NativeStackScreenProps {!visible && } {visible && pickupLocation && dropoffLocation && ( - + {loaded && ( diff --git a/mobile/screens/CheckoutScreen.tsx b/mobile/screens/CheckoutScreen.tsx index c9ce447c9..1f2ed8211 100644 --- a/mobile/screens/CheckoutScreen.tsx +++ b/mobile/screens/CheckoutScreen.tsx @@ -85,6 +85,14 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps(null) @@ -233,7 +241,7 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps val === 0 @@ -246,6 +254,16 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps { + const onCancellationChange = async (checked: boolean) => { const options = { cancellation: checked, amendments, @@ -464,12 +482,12 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps { + const onAmendmentsChange = async (checked: boolean) => { const options = { cancellation, amendments: checked, @@ -478,12 +496,12 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps { + const onCollisionDamageWaiverChange = async (checked: boolean) => { const options = { cancellation, amendments, @@ -492,12 +510,12 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps { + const onTheftProtectionChange = async (checked: boolean) => { const options = { cancellation, amendments, @@ -506,12 +524,12 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps { + const onFullInsuranceChange = async (checked: boolean) => { const options = { cancellation, amendments, @@ -520,12 +538,12 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps { + const onAdditionalDriverChange = async (checked: boolean) => { const options = { cancellation, amendments, @@ -534,7 +552,7 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps {formVisible && ( {i18n.t('CREATE_BOOKING')}} footerComponent={ @@ -786,7 +808,7 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps - {helper.getCancellationOption(car.cancellation, language)} + {cancellationText} @@ -797,29 +819,29 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps - {helper.getAmendmentsOption(car.amendments, language)} + {amendmentsText} - {helper.getCollisionDamageWaiverOption(car.collisionDamageWaiver, days, language)} + {theftProtectionText} - {helper.getTheftProtectionOption(car.theftProtection, days, language)} + {collisionDamageWaiverText} @@ -830,7 +852,7 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps - {helper.getFullInsuranceOption(car.fullInsurance, days, language)} + {fullInsuranceText} @@ -841,7 +863,7 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps - {helper.getAdditionalDriverOption(car.additionalDriver, days, language)} + {additionalDriverText} @@ -865,7 +887,7 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps{dropOffLocation.name} {i18n.t('CAR')} - {`${car.name} (${bookcarsHelper.formatPrice(price / days, i18n.t('CURRENCY'), language)}${i18n.t('DAILY')})`} + {`${car.name} (${bookcarsHelper.formatPrice(price / days, currencySymbol, language)}${i18n.t('DAILY')})`} {i18n.t('SUPPLIER')} @@ -879,7 +901,7 @@ const CheckoutScreen = ({ navigation, route }: NativeStackScreenProps {i18n.t('COST')} - {`${bookcarsHelper.formatPrice(price, i18n.t('CURRENCY'), language)}`} + {`${bookcarsHelper.formatPrice(price, currencySymbol, language)}`} {!authenticated && ( diff --git a/mobile/services/StripeService.ts b/mobile/services/StripeService.ts index b4fa4a795..2a60a9005 100644 --- a/mobile/services/StripeService.ts +++ b/mobile/services/StripeService.ts @@ -1,5 +1,8 @@ import * as bookcarsTypes from ':bookcars-types' +import * as bookcarsHelper from ':bookcars-helper' import axiosInstance from './axiosInstance' +import * as env from '@/config/env.config' +import * as AsyncStorage from '@/common/AsyncStorage' /** * Create Payment Intent @@ -14,3 +17,69 @@ export const createPaymentIntent = (payload: bookcarsTypes.CreatePaymentPayload) payload ) .then((res) => res.data) + +/** +* Set currency. +* +* @param {string} currency +*/ +export const setCurrency = async (currency: string) => { + if (currency && bookcarsHelper.checkCurrency(currency.toUpperCase())) { + await AsyncStorage.storeString('bc-currency', currency.toUpperCase()) + } +} + +/** + * Get currency. + * + * @returns {string} + */ +export const getCurrency = async () => { + const currency = await AsyncStorage.getString('bc-currency') + if (currency && bookcarsHelper.checkCurrency(currency.toUpperCase())) { + return currency.toUpperCase() + } + return env.BASE_CURRENCY +} + +/** + * Return currency symbol. + * + * @param {string} code + * @returns {string|undefined} + */ +export const getCurrencySymbol = async () => { + const currency = await getCurrency() + const currencySymbol = env.CURRENCIES.find((c) => c.code === currency)?.symbol || '$' + return currencySymbol +} + +/** + * Convert a price to a given currency. + * + * @async + * @param {number} amount + * @param {string} to + * @returns {Promise} + */ +export const convertPrice = async (amount: number) => { + const to = await getCurrency() + + if (to !== env.BASE_CURRENCY) { + const res = await bookcarsHelper.convertPrice(amount, env.BASE_CURRENCY, to) + return res + } + + return amount +} + +/** + * Check if currency is written from right to left. + * + * @returns {*} + */ +export const currencyRTL = async () => { + const currencySymbol = await getCurrencySymbol() + const isRTL = bookcarsHelper.currencyRTL(currencySymbol) + return isRTL +} diff --git a/mobile/tsconfig.json b/mobile/tsconfig.json index bb85b0efb..1b95b69bb 100644 --- a/mobile/tsconfig.json +++ b/mobile/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "expo/tsconfig.base", "compilerOptions": { + "module": "ESNext", "strict": true, "typeRoots": [ "./types" diff --git a/mobile/types/env.d.ts b/mobile/types/env.d.ts index f61cdafa1..4d3265bb2 100644 --- a/mobile/types/env.d.ts +++ b/mobile/types/env.d.ts @@ -16,8 +16,7 @@ declare module '@env' { export const BC_STRIPE_PUBLISHABLE_KEY: string export const BC_STRIPE_MERCHANT_IDENTIFIER: string export const BC_STRIPE_COUNTRY_CODE: string - export const BC_STRIPE_CURRENCY_CODE: string - export const BC_CURRENCY: string + export const BC_BASE_CURRENCY: string export const BC_DEPOSIT_FILTER_VALUE_1: string export const BC_DEPOSIT_FILTER_VALUE_2: string export const BC_DEPOSIT_FILTER_VALUE_3: string diff --git a/packages/bookcars-helper/package-lock.json b/packages/bookcars-helper/package-lock.json index 8646f22c2..8d4852880 100644 --- a/packages/bookcars-helper/package-lock.json +++ b/packages/bookcars-helper/package-lock.json @@ -8,6 +8,9 @@ "name": "bookcars-helper", "version": "1.0.0", "license": "ISC", + "dependencies": { + "@babel/runtime": "^7.26.0" + }, "devDependencies": { "rimraf": "^5.0.5", "typescript": "^5.2.2" @@ -22,6 +25,18 @@ "typescript": "^5.2.2" } }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -261,6 +276,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/rimraf": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", diff --git a/packages/bookcars-helper/package.json b/packages/bookcars-helper/package.json index 294bb3ead..c7cbe95d8 100644 --- a/packages/bookcars-helper/package.json +++ b/packages/bookcars-helper/package.json @@ -12,5 +12,8 @@ "devDependencies": { "rimraf": "^5.0.5", "typescript": "^5.2.2" + }, + "dependencies": { + "@babel/runtime": "^7.26.0" } } diff --git a/packages/currency-converter/package-lock.json b/packages/currency-converter/package-lock.json index 747c2e60a..91139bf99 100644 --- a/packages/currency-converter/package-lock.json +++ b/packages/currency-converter/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@babel/runtime": "^7.26.0", "easy-currencies": "^1.8.1" }, "devDependencies": { @@ -17,6 +18,18 @@ "typescript": "^5.7.2" } }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -389,6 +402,12 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/rimraf": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", diff --git a/packages/currency-converter/package.json b/packages/currency-converter/package.json index e3039429e..992ae3d2d 100644 --- a/packages/currency-converter/package.json +++ b/packages/currency-converter/package.json @@ -11,6 +11,7 @@ "author": "Akram El Assas", "license": "ISC", "dependencies": { + "@babel/runtime": "^7.26.0", "easy-currencies": "^1.8.1" }, "devDependencies": {