diff --git a/examples/client/Locomotion/src/Components/BsPages/ActiveRide/index.tsx b/examples/client/Locomotion/src/Components/BsPages/ActiveRide/index.tsx index c1b0f4bf9..d835a94b9 100644 --- a/examples/client/Locomotion/src/Components/BsPages/ActiveRide/index.tsx +++ b/examples/client/Locomotion/src/Components/BsPages/ActiveRide/index.tsx @@ -165,7 +165,7 @@ const ActiveRideContent = () => { ride={ride} /> ( + + {title} + {subTitle} + +); + +export default BusinessAccountText; diff --git a/examples/client/Locomotion/src/Components/BusinessAccountText/styled.ts b/examples/client/Locomotion/src/Components/BusinessAccountText/styled.ts new file mode 100644 index 000000000..8489378fb --- /dev/null +++ b/examples/client/Locomotion/src/Components/BusinessAccountText/styled.ts @@ -0,0 +1,22 @@ +import { Text, View } from 'react-native'; +import styled from 'styled-components'; +import { FONT_SIZES, FONT_WEIGHTS } from '../../context/theme'; + +export const TitleWithSubTitle = styled(View)` + display: flex; + flex-direction: column; + flex: 1 0 0; +`; +export const BaseText = styled(Text)` +color: #212229; +${FONT_SIZES.LARGE}; +font-family: Inter; +font-style: normal; +line-height: 20px; +`; +export const SubTitle = styled(BaseText)` +font-weight: 500; +`; +export const BoldTitle = styled(BaseText)` +font-weight: 700; +`; diff --git a/examples/client/Locomotion/src/Components/CardRow/index.tsx b/examples/client/Locomotion/src/Components/CardRow/index.tsx index 65aaab089..aaba9c0e3 100644 --- a/examples/client/Locomotion/src/Components/CardRow/index.tsx +++ b/examples/client/Locomotion/src/Components/CardRow/index.tsx @@ -5,6 +5,8 @@ import { View, Text } from 'react-native'; import moment from 'moment'; import styled, { ThemeContext } from 'styled-components'; import { PaymentIcon } from 'react-native-payment-icons'; +import BusinessAccountText from '../BusinessAccountText'; +import { RideInterface, RidePageContext } from '../../context/newRideContext'; import { PAYMENT_METHODS, paymentMethodToIconMap } from '../../pages/Payments/consts'; import Button from '../Button'; import { capitalizeFirstLetter, getLastFourForamttedShort } from '../../pages/Payments/cardDetailUtils'; @@ -13,7 +15,7 @@ import SvgIcon from '../SvgIcon'; import selected from '../../assets/selected-v.svg'; import { Start, StartCapital } from '../../lib/text-direction'; import chevronIcon from '../../assets/chevron.svg'; -import { isCashPaymentMethod } from '../../lib/ride/utils'; +import { isCashPaymentMethod, isOfflinePaymentMethod } from '../../lib/ride/utils'; import paymentContext from '../../context/payments'; type ContainerProps = { @@ -93,9 +95,28 @@ const style = { const CardRow = (paymentMethod: any) => { const { primaryColor } = useContext(ThemeContext); - const { offlinePaymentText, loadOfflinePaymentText } = paymentContext.useContainer(); + const { + offlinePaymentText, + loadOfflinePaymentText, + getBusinessAccountNameById, + } = paymentContext.useContainer(); + const { businessAccountId } = paymentMethod; const [isCardExpired, setIsCardExpired] = useState(false); + const getPaymentMethodTitle = () => { + const businessAccountName = getBusinessAccountNameById(businessAccountId); + if (businessAccountName) { + return businessAccountName; + } + if (isCashPaymentMethod(paymentMethod)) { + return i18n.t('payments.cash'); + } + if (isOfflinePaymentMethod(paymentMethod)) { + return offlinePaymentText; + } + return capitalizeFirstLetter(paymentMethod.name); + }; + useEffect(() => { loadOfflinePaymentText(); }, []); @@ -111,11 +132,15 @@ const CardRow = (paymentMethod: any) => { : (`${paymentMethod.testIdPrefix || ''}ChoosePaymentMethod${paymentMethod.id === PAYMENT_METHODS.OFFLINE || paymentMethod.id === PAYMENT_METHODS.CASH ? `_${paymentMethod.id}` : ''}`); const getPaymentMethodIcon = () => { + if (paymentMethod.noSvg) { + return null; + } const { brand, id, lastFour } = paymentMethod; const isCard = lastFour; if (isCard) { return ; } + if (!paymentMethodToIconMap[id]) { return null; } return ( { : ( <> {getPaymentMethodIcon()} - {paymentMethod.mark ? ( + {(paymentMethod.mark && !paymentMethod.alignMarkToRight) ? ( { ) : ( <> - {!paymentMethod.lastFour - ? ( - - {isCashPaymentMethod(paymentMethod) ? i18n.t('payments.cash') : offlinePaymentText } - - ) - : ( - - {capitalizeFirstLetter(paymentMethod.name)} - - )} + { + (businessAccountId && offlinePaymentText) + ? ( + + ) + : ( + + {getPaymentMethodTitle()} + + )} {paymentMethod.lastFour ? {getLastFourForamttedShort(paymentMethod.lastFour)} : null} @@ -205,6 +232,17 @@ const CardRow = (paymentMethod: any) => { {paymentMethod.disabledReason} )} + {(paymentMethod.mark && paymentMethod.alignMarkToRight) ? ( + + ) : null } diff --git a/examples/client/Locomotion/src/Components/EmptyState/index.tsx b/examples/client/Locomotion/src/Components/EmptyState/index.tsx new file mode 100644 index 000000000..998270cca --- /dev/null +++ b/examples/client/Locomotion/src/Components/EmptyState/index.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { + Container, Description, Title, TitleWithoutDescription, +} from './styled'; + +interface EmptyStateProps { + title: string; + description?: string; +} +const EmptyState = ({ + title, + description, +}: EmptyStateProps) => ( + + {description + ? {title} + : {title} + } + + {description ? {description} : null} + +); +EmptyState.defaultProps = { + description: '', +}; +export default EmptyState; diff --git a/examples/client/Locomotion/src/Components/EmptyState/styled.ts b/examples/client/Locomotion/src/Components/EmptyState/styled.ts new file mode 100644 index 000000000..a67573b6d --- /dev/null +++ b/examples/client/Locomotion/src/Components/EmptyState/styled.ts @@ -0,0 +1,40 @@ +import { Text, View } from 'react-native'; +import styled from 'styled-components'; +import { FONT_SIZES, FONT_WEIGHTS } from '../../context/theme'; + +export const Container = styled(View)` +border-radius: 8px; +border: 1px dashed rgba(125, 139, 172, 0.32); +display: flex; +padding: 16px; +flex-direction: column; +justify-content: center; +align-items: center; +margin: 4px; +flex: 1 0 0; +align-self: stretch; +`; +export const Title = styled(Text)` +align-self: stretch; +color: #212229; +text-align: center; +${FONT_WEIGHTS.REGULAR}; +${FONT_SIZES.LARGE}; +font-weight: 600; +`; +export const Description = styled(Text)` +align-self: stretch; +color: #666975; +text-align: center; +${FONT_WEIGHTS.REGULAR}; +${FONT_SIZES.LARGE}; +font-weight: 400; +`; +export const TitleWithoutDescription = styled(Text)` +align-self: stretch; +color: #666975; +text-align: center; +${FONT_WEIGHTS.REGULAR}; +${FONT_SIZES.LARGE}; +font-weight: 500; +`; diff --git a/examples/client/Locomotion/src/Components/RideCard/index.tsx b/examples/client/Locomotion/src/Components/RideCard/index.tsx index dda8dc2b0..30e0a9d2f 100644 --- a/examples/client/Locomotion/src/Components/RideCard/index.tsx +++ b/examples/client/Locomotion/src/Components/RideCard/index.tsx @@ -13,6 +13,7 @@ import { import StopPointsVerticalView from '../StopPointsVerticalView'; import { getFormattedPrice, isPriceEstimated, convertTimezoneByLocation } from '../../context/newRideContext/utils'; import cashIcon from '../../assets/cash.svg'; +import offlineIcon from '../../assets/offline.svg'; import { PAYMENT_METHODS } from '../../pages/Payments/consts'; import PaymentContext from '../../context/payments'; import SettingsContext from '../../context/settings'; @@ -23,17 +24,26 @@ interface CardComponentProps { brand: any; id: string; } + businessAccountId: string | undefined; } -const CardComponent = ({ paymentMethod }: CardComponentProps) => { +const CardComponent = ({ paymentMethod, businessAccountId }: CardComponentProps) => { const isCash = PAYMENT_METHODS.CASH === paymentMethod.id; const isOffline = PAYMENT_METHODS.OFFLINE === paymentMethod.id; - const { offlinePaymentText, loadOfflinePaymentText } = PaymentContext.useContainer(); + const { + offlinePaymentText, + loadOfflinePaymentText, + getBusinessAccountNameById, + } = PaymentContext.useContainer(); useEffect(() => { loadOfflinePaymentText(); }, []); const getText = () => { + const businessAccountName = getBusinessAccountNameById(businessAccountId); + if (businessAccountName) { + return businessAccountName; + } if (isCash) { return i18n.t('payments.cash'); } if (isOffline) { @@ -47,12 +57,13 @@ const CardComponent = ({ paymentMethod }: CardComponentProps) => { return cashIcon; } if (isOffline) { - return null; + return offlineIcon; } }; return ( !isCash && !isOffline && } icon={getIcon()} style={{ marginTop: 10, marginBottom: 10 }} @@ -168,7 +179,12 @@ const RideCard = ({ - {paymentMethod && } + {paymentMethod && ( + + )} {i18n.t('home.cancelRideButton')} diff --git a/examples/client/Locomotion/src/Components/RidePaymentDetails/index.tsx b/examples/client/Locomotion/src/Components/RidePaymentDetails/index.tsx index 887ce9f17..a1e2e4a41 100644 --- a/examples/client/Locomotion/src/Components/RidePaymentDetails/index.tsx +++ b/examples/client/Locomotion/src/Components/RidePaymentDetails/index.tsx @@ -6,7 +6,7 @@ import { getFormattedPrice } from '../../context/newRideContext/utils'; import CardRow from '../CardRow'; import CardsTitle from '../CardsTitle'; import i18n from '../../I18n'; -import { PriceCalculation, RidePageContext } from '../../context/newRideContext'; +import { PriceCalculation, RideInterface, RidePageContext } from '../../context/newRideContext'; import { PaymentRow, RidePriceDetails, PriceText, ViewDetails, CardRowContainer, } from './styled'; @@ -16,12 +16,12 @@ import * as navigationService from '../../services/navigation'; import Button from '../Button'; const RidePaymentDetails = ({ - rideId, + ride, paymentMethod, rideHistory = false, state, } :{ - rideId: string, + ride: RideInterface, paymentMethod: PaymentMethodInterface, rideHistory: boolean currency: string, @@ -36,7 +36,7 @@ const RidePaymentDetails = ({ const { showPrice, loadShowPrice } = SettingsContext.useContainer(); const updatePriceCalculation = async () => { - const calculation = await getRidePriceCalculation(rideId); + const calculation = await getRidePriceCalculation(ride?.id); setPriceCalculation(calculation); }; @@ -53,7 +53,7 @@ const RidePaymentDetails = ({ - + @@ -74,7 +74,7 @@ const RidePaymentDetails = ({ testID="viewRidePaymentDetails" noBackground onPress={() => navigationService.navigate(MAIN_ROUTES.RIDE_PRICE_BREAKDOWN, - { rideId, rideHistory })} + { rideId: ride.id, rideHistory })} > {state !== RIDE_STATES.CANCELED || (state === RIDE_STATES.CANCELED diff --git a/examples/client/Locomotion/src/Components/TabSwitch/index.tsx b/examples/client/Locomotion/src/Components/TabSwitch/index.tsx new file mode 100644 index 000000000..c95db56e7 --- /dev/null +++ b/examples/client/Locomotion/src/Components/TabSwitch/index.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import SvgIcon from '../SvgIcon'; +import i18n from '../../I18n'; +import { + Container, SELECTED_COLOR, Tab, TabInner, TextContainer, UNSELECTED_COLOR, +} from './styled'; + +interface ITabSwitchProps { + onUnselectedClick: (tab) => void + activeTabId: string; + tabs: { + textKey: string; + id: string; + Svg: any; + }[]; +} + +const TabSwitch = ({ onUnselectedClick, tabs, activeTabId }: ITabSwitchProps) => ( + + + {tabs.map((tab) => { + const isSelected = tab.id === activeTabId; + return ( + { + if (!isSelected) { + onUnselectedClick(tab); + } + }} + isSelected={isSelected} + > + + + {tab.Svg + && ( + + ) + } + + {i18n.t(tab.textKey)} + + + + + ); + })} + + +); +export default TabSwitch; diff --git a/examples/client/Locomotion/src/Components/TabSwitch/styled.ts b/examples/client/Locomotion/src/Components/TabSwitch/styled.ts new file mode 100644 index 000000000..7159b703c --- /dev/null +++ b/examples/client/Locomotion/src/Components/TabSwitch/styled.ts @@ -0,0 +1,58 @@ +import styled from 'styled-components/native'; + +interface TabStyled { + isSelected: boolean; +} + +export const SELECTED_COLOR = '#212229'; +export const UNSELECTED_COLOR = '#666975'; + +export const Container = styled.View` + flex-direction: row; + justify-content: center; + align-items: center; + align-self: stretch; + border-color: rgba(125, 139, 172, 0.32) + border-bottom-width: 1px; + border-bottom-color: #7D8BAC52; + margin-bottom: 16px; + padding-left: 8px; + padding-right: 8px; + overflow: hidden; +`; + +export const Tab = styled.TouchableOpacity` + height: 40px; + justify-content: center; + align-items: center; + flex: 1 0 0; + text-align: center; + ${({ isSelected }: TabStyled) => isSelected && `border-bottom-width: 2px; border-bottom-color: ${SELECTED_COLOR};`} + margin-left: 8px; + margin-right: 8px; + overflow: hidden; +`; +export const TabInner = styled.View` +display: flex; +flex-direction: row; +height: 32px; +padding: 4px; +justify-content: center; +align-items: center; +color: ${({ isSelected }: TabStyled) => (isSelected ? SELECTED_COLOR : UNSELECTED_COLOR)}; +font-family: Inter; +font-size: 16px; +font-style: normal; +font-weight: 500; +line-height: 24px; +`; +export const TextContainer = styled.Text` +color: #666975; +color: ${({ isSelected }: TabStyled) => (isSelected ? SELECTED_COLOR : UNSELECTED_COLOR)}; +font-family: Inter; +font-size: 16px; +font-style: normal; +font-weight: 500; +line-height: 24px; +padding-left: 4px; +`; diff --git a/examples/client/Locomotion/src/Components/TextRowWithIcon/index.tsx b/examples/client/Locomotion/src/Components/TextRowWithIcon/index.tsx index a970c67d0..4d801026a 100644 --- a/examples/client/Locomotion/src/Components/TextRowWithIcon/index.tsx +++ b/examples/client/Locomotion/src/Components/TextRowWithIcon/index.tsx @@ -1,6 +1,7 @@ import React, { useContext } from 'react'; import { Text, View } from 'react-native'; import styled, { ThemeContext } from 'styled-components'; +import BusinessAccountText from '../BusinessAccountText'; import { FONT_SIZES } from '../../context/theme'; import SvgIcon from '../SvgIcon'; @@ -31,7 +32,7 @@ interface TextRowWithIconProps { } const TextRowWithIcon = ({ - text, icon, style, Image, iconWidth, iconHeight, + subTitle, text, icon, style, Image, iconWidth, iconHeight, }: TextRowWithIconProps) => { const theme = useContext(ThemeContext); const getImage = () => { @@ -56,7 +57,13 @@ const TextRowWithIcon = ({ ...((Image || icon) && { marginLeft: 10 }), }} > - {text} + {subTitle ? ( + + ) + : text} ); diff --git a/examples/client/Locomotion/src/I18n/en.json b/examples/client/Locomotion/src/I18n/en.json index bdd7eade1..56d4da2f4 100644 --- a/examples/client/Locomotion/src/I18n/en.json +++ b/examples/client/Locomotion/src/I18n/en.json @@ -45,6 +45,10 @@ "tags": { "fastest": "Fastest", "cheapest": "Cheapest" + }, + "emptyState": { + "title": "There are no available services", + "description": "Try changing the ride schedule or select a different payment method" } }, "rideDetails": { @@ -289,7 +293,12 @@ "placeholder": "How to find me, flights, info, etc.", "save": "Save", "cancel": "Cancel", - "unavailable": "Unavailable for the selected service" + "unavailable": "Unavailable for the selected service", + "emptyStateText": "No credit cards were added", + "tabs": { + "personal": "Personal", + "business": "Business" + } }, "rideSummary": { "thanksForRideHeadline": "Thanks for your ride!", diff --git a/examples/client/Locomotion/src/assets/business-payment.svg b/examples/client/Locomotion/src/assets/business-payment.svg new file mode 100644 index 000000000..1f28c0949 --- /dev/null +++ b/examples/client/Locomotion/src/assets/business-payment.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/client/Locomotion/src/assets/offline.svg b/examples/client/Locomotion/src/assets/offline.svg index 6d5ea4f51..66a39d4e8 100644 --- a/examples/client/Locomotion/src/assets/offline.svg +++ b/examples/client/Locomotion/src/assets/offline.svg @@ -1,4 +1,3 @@ - - - - + + + \ No newline at end of file diff --git a/examples/client/Locomotion/src/assets/personal-payment.svg b/examples/client/Locomotion/src/assets/personal-payment.svg new file mode 100644 index 000000000..82c93b8af --- /dev/null +++ b/examples/client/Locomotion/src/assets/personal-payment.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/client/Locomotion/src/context/newRideContext/api.js b/examples/client/Locomotion/src/context/newRideContext/api.js index ceeb865b0..c212a9373 100644 --- a/examples/client/Locomotion/src/context/newRideContext/api.js +++ b/examples/client/Locomotion/src/context/newRideContext/api.js @@ -1,8 +1,8 @@ import network from '../../services/network'; -export const createServiceEstimations = async (stopPoints, scheduledTo) => { +export const createServiceEstimations = async (stopPoints, scheduledTo, businessAccountId) => { try { - const { data } = await network.post('api/v1/services/service-estimations', { stopPoints, scheduledTo }); + const { data } = await network.post('api/v1/services/service-estimations', { stopPoints, scheduledTo, businessAccountId }); return data; } catch (e) { console.error(e); diff --git a/examples/client/Locomotion/src/context/newRideContext/index.tsx b/examples/client/Locomotion/src/context/newRideContext/index.tsx index 038d28d6c..a1c832631 100644 --- a/examples/client/Locomotion/src/context/newRideContext/index.tsx +++ b/examples/client/Locomotion/src/context/newRideContext/index.tsx @@ -8,12 +8,15 @@ import { useNavigation } from '@react-navigation/native'; import _, { pick } from 'lodash'; import moment, { Moment } from 'moment-timezone'; import debounce from 'lodash/debounce'; +import { PAYMENT_MODES } from '../../pages/Payments/consts'; +import offlinePaymentMethod from '../../pages/Payments/offlinePaymentMethod'; import i18n from '../../I18n'; import { FutureRidesContext } from '../futureRides'; import { UserContext } from '../user'; +import PaymentContext from '../payments'; import { getPosition, DEFAULT_COORDS } from '../../services/geo'; import { - getPlaces, getGeocode, getPlaceDetails, getLocationTimezone, + getPlaces, getGeocode, getPlaceDetails, } from './google-api'; import StorageService from '../../services/storage'; import Mixpanel from '../../services/Mixpanel'; @@ -78,6 +81,7 @@ export interface RideInterface { priceCalculationId?: string; rideFeedbacks?: RideFeedback[]; cancellationReasonId?: string; + businessAccountId?: string; } type AdditionalCharge = { @@ -160,6 +164,8 @@ interface RidePageContextInterface { setLastAcknowledgedRideCompletionTimestampToNow: () => void loadFutureBookingDays: () => void; futureBookingDays: number; + businessAccountId: string | null, + updateBusinessAccountId: (newBusinessAccountId: string | null) => void; } export const RidePageContext = createContext({ @@ -219,6 +225,8 @@ export const RidePageContext = createContext({ setLastAcknowledgedRideCompletionTimestampToNow: () => undefined, loadFutureBookingDays: () => undefined, futureBookingDays: 0, + businessAccountId: null, + updateBusinessAccountId: (newBusinessAccountId: string | null) => undefined, }); const HISTORY_RECORDS_NUM = 10; @@ -226,6 +234,7 @@ const HISTORY_RECORDS_NUM = 10; const RidePageContextProvider = ({ children }: { children: any }) => { + const { getClientDefaultMethod } = PaymentContext.useContainer(); const { locationGranted, user } = useContext(UserContext); const { isStationsEnabled, @@ -258,17 +267,21 @@ const RidePageContextProvider = ({ children }: { const [numberOfPassengers, setNumberOfPassengers] = useState(null); const [addressSearchLabel, setAddressSearchLabel] = useState(null); const [futureBookingDays, setFutureBookingDays] = useState(0); - + const [businessAccountId, setBusinessAccountId] = useState(null); const intervalRef = useRef(); const stopRequestInterval = () => { clearInterval(intervalRef.current); }; - const saveLastRide = async (rideId: string) => { await StorageService.save({ lastRideId: rideId }); }; + const saveOrderedRidePaymentMethod = async (rideBusinessAccountId: string | null) => Promise.all([ + StorageService.save({ lastBusinessAccountId: rideBusinessAccountId || PAYMENT_MODES.PERSONAL }), + StorageService.save({ orderedRide: true }), + ]); + const clearLastRide = async () => { await StorageService.delete('lastRideId'); @@ -278,6 +291,7 @@ const RidePageContextProvider = ({ children }: { setRequestStopPoints(INITIAL_STOP_POINTS); setChosenService(null); setDefaultService(null); + setBusinessAccountId(null); }; const cleanRideState = (initSpsBool = true) => { @@ -286,6 +300,7 @@ const RidePageContextProvider = ({ children }: { } setRide({}); clearLastRide(); + setBusinessAccountId(null); }; const onRideCompleted = (rideId: string, priceCalculationId: string) => { @@ -370,9 +385,21 @@ const RidePageContextProvider = ({ children }: { }); }; + const updateRidePayload = (newRide: RideInterface) => { + setRide({ + ...ride, + ...newRide, + }); + }; + const FAILED_ESTIMATIONS_ACTIONS = { [ESTIMATION_ERRORS['RIDE_VALIDATION:SOME_STOP_POINTS_ARE_OUT_OF_TERRITORY']]: () => changeBsPage(BS_PAGES.NOT_IN_TERRITORY), [ESTIMATION_ERRORS.FIRST_STOP_POINT_NOT_IN_TERRITORY]: () => changeBsPage(BS_PAGES.PICKUP_NOT_IN_TERRITORY), + [ESTIMATION_ERRORS.CLIENT_NOT_IN_BUSINESS_ACCOUNT]: async () => { + await StorageService.delete('lastBusinessAccountId'); + setBusinessAccountId(null); + updateRidePayload({ paymentMethodId: undefined }); + }, }; @@ -381,7 +408,26 @@ const RidePageContextProvider = ({ children }: { return timezoneResponse.time; }; - const getServiceEstimations = async (throwError = true) => { + + const getBusinessAccountIdWithFallback = async (paymentChosen: boolean) => { + const doNotUseFallback = paymentChosen || businessAccountId; + if (doNotUseFallback) { + return businessAccountId; + } + const [notFirstRide, fallbackId] = await Promise.all([ + StorageService.get('orderedRide'), + StorageService.get('lastBusinessAccountId'), + ]); + const defaultPaymentMethod = notFirstRide ? getClientDefaultMethod() : null; + const usePersonalPayment = !fallbackId || fallbackId === PAYMENT_MODES.PERSONAL || defaultPaymentMethod; + if (usePersonalPayment) { return null; } + updateRidePayload({ paymentMethodId: offlinePaymentMethod.id }); + setBusinessAccountId(fallbackId); + return fallbackId; + }; + + const getServiceEstimations = async (throwError = true, paymentChosen = true) => { + const relevantBusinessAccountId = await getBusinessAccountIdWithFallback(paymentChosen); changeBsPage(BS_PAGES.SERVICE_ESTIMATIONS); try { const formattedStopPoints = formatStopPointsForEstimations(requestStopPoints); @@ -392,9 +438,8 @@ const RidePageContextProvider = ({ children }: { const unixScheduledTo = moment.unix(Number(ride.scheduledTo) / 1000); scheduledTime = await getLocationTimezoneTime(formattedStopPoints[0].lat, formattedStopPoints[0].lng, unixScheduledTo); } - const { estimations, services } = await rideApi - .createServiceEstimations(formattedStopPoints, scheduledTime); + .createServiceEstimations(formattedStopPoints, scheduledTime, relevantBusinessAccountId); const tags = getEstimationTags(estimations); const formattedEstimations = formatEstimations(services, estimations, tags); @@ -405,8 +450,11 @@ const RidePageContextProvider = ({ children }: { } catch (e: any) { Mixpanel.setEvent('service estimations failed', { status: e?.response?.status }); if (throwError) { - if (FAILED_ESTIMATIONS_ACTIONS[e?.response?.data?.errors[0]]) { - FAILED_ESTIMATIONS_ACTIONS[e?.response?.data?.errors[0]](); + const error = e?.response?.data?.errors[0]; + const errorHandleFunction = FAILED_ESTIMATIONS_ACTIONS[error]; + if (errorHandleFunction) { + Mixpanel.setEvent('service estimations failed with error reason', { error }); + await errorHandleFunction(); } else { cleanRideState(); setRidePopup(RIDE_POPUPS.FAILED_SERVICE_REQUEST); @@ -433,24 +481,24 @@ const RidePageContextProvider = ({ children }: { } }; - const tryServiceEstimations = async () => { + const tryServiceEstimations = async (paymentChosen = true) => { const serviceEstimationsInterval = await getServiceEstimationsFetchingInterval(); - await getServiceEstimations(); + await getServiceEstimations(true, paymentChosen); clearInterval(intervalRef.current); intervalRef.current = setInterval(async () => { if (intervalRef.current) { - await getServiceEstimations(false); + await getServiceEstimations(false, true); } }, ((serviceEstimationsInterval || 60) * 1000)); }; const validateStopPointInTerritory = (stopPoints: any[]) => checkStopPointsInTerritory(stopPoints); - const validateRequestedStopPoints = (reqSps: any[]) => { + const validateRequestedStopPoints = (reqSps: any[], paymentChosen = true) => { const stopPoints = reqSps; const isSpsReady = stopPoints.every(r => r.lat && r.lng && r.description); if (stopPoints.length && isSpsReady) { - tryServiceEstimations(); + tryServiceEstimations(paymentChosen); } else if (![BS_PAGES.ADDRESS_SELECTOR, BS_PAGES.LOADING].includes(currentBsPage)) { // reset req stop point request if (!ride.id) { @@ -554,6 +602,13 @@ const RidePageContextProvider = ({ children }: { } }, [user?.id]); + useEffect(() => { + if (currentBsPage === BS_PAGES.SERVICE_ESTIMATIONS) { + setServiceEstimations(null); + tryServiceEstimations(); + } + }, [businessAccountId]); + useEffect(() => { if (user?.id && isAppActive && !ride.id) { loadLastCompletedRide(); @@ -594,7 +649,7 @@ const RidePageContextProvider = ({ children }: { }, 4000); useEffect(() => { - validateRequestedStopPoints(requestStopPoints); + validateRequestedStopPoints(requestStopPoints, false); }, [requestStopPoints]); const getRideFromApi = async (rideId: string): Promise => formatRide(await rideApi.getRide(rideId)); @@ -1081,7 +1136,6 @@ const RidePageContextProvider = ({ children }: { const unixScheduledTo = moment.unix(Number(ride.scheduledTo) / 1000); scheduledToMoment = await getLocationTimezoneTime(pickupLocation.lat, pickupLocation.lng, unixScheduledTo); } - const rideToCreate = { serviceId: chosenService?.id, estimationId: chosenService?.estimationId, @@ -1096,10 +1150,14 @@ const RidePageContextProvider = ({ children }: { type: sp.type, ...(i === 0 && { notes: ride.notes }), })), + ...(businessAccountId ? { businessAccountId } : {}), }; - const afRide = await rideApi.createRide(rideToCreate); + const [afRide] = await Promise.all([ + rideApi.createRide(rideToCreate), + saveOrderedRidePaymentMethod(businessAccountId), + ]); if (afRide.state === RIDE_STATES.REJECTED) { throw new Error(RIDE_FAILED_REASONS.BUSY); } @@ -1112,8 +1170,6 @@ const RidePageContextProvider = ({ children }: { setRide(formattedRide); } } catch (e: any) { - console.log(e); - console.log(e.message); const key = e.response?.data?.errors[0] || e.message; Mixpanel.setEvent('Ride failed', { status: e?.response?.status, reason: key }); if (FAILED_TO_CREATE_RIDE_ACTIONS[key]) { @@ -1134,12 +1190,6 @@ const RidePageContextProvider = ({ children }: { } }; - const updateRidePayload = (newRide: RideInterface) => { - setRide({ - ...ride, - ...newRide, - }); - }; const patchRideRating = async (rideId: string, rating: number | null, feedback: RideFeedback | null): Promise => { if (!rating && !feedback) { @@ -1290,6 +1340,11 @@ const RidePageContextProvider = ({ children }: { streetAddress: null, }, index); }; + const updateBusinessAccountId = (newBusinessAccountId: string | null) => { + if (newBusinessAccountId !== businessAccountId) { + setBusinessAccountId(newBusinessAccountId); + } + }; return ( {children} diff --git a/examples/client/Locomotion/src/context/newRideContext/utils.ts b/examples/client/Locomotion/src/context/newRideContext/utils.ts index db677a66a..521da5d8d 100644 --- a/examples/client/Locomotion/src/context/newRideContext/utils.ts +++ b/examples/client/Locomotion/src/context/newRideContext/utils.ts @@ -7,6 +7,7 @@ import { getLocationTimezone } from './api'; export const ESTIMATION_ERRORS = { 'RIDE_VALIDATION:SOME_STOP_POINTS_ARE_OUT_OF_TERRITORY': 'RIDE_VALIDATION:SOME_STOP_POINTS_ARE_OUT_OF_TERRITORY', FIRST_STOP_POINT_NOT_IN_TERRITORY: 'FIRST_STOP_POINT_NOT_IN_TERRITORY', + CLIENT_NOT_IN_BUSINESS_ACCOUNT: 'BUSINESS_ACCOUNT_CLIENT_NOT_FOUND', }; export const RIDE_FAILED_REASONS = { diff --git a/examples/client/Locomotion/src/context/payments/index.js b/examples/client/Locomotion/src/context/payments/index.js index 8d158be88..0f20b1099 100644 --- a/examples/client/Locomotion/src/context/payments/index.js +++ b/examples/client/Locomotion/src/context/payments/index.js @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import { createContainer } from 'unstated-next'; +import { StorageService } from '../../services'; import i18n from '../../I18n'; import { PAYMENT_STATES } from '../../lib/commonTypes'; import Mixpanel from '../../services/Mixpanel'; @@ -17,6 +18,7 @@ const usePayments = () => { const [paymentAccount, setPaymentAccount] = useState(null); const [hasOutstandingPayment, setHasOutstandingPayment] = useState(false); const [offlinePaymentText, setOfflinePaymentText] = useState(null); + const [businessPaymentMethods, setBusinessPaymentMethods] = useState([]); const loadOfflinePaymentText = async () => { const companyName = await useSettings.getSettingByKey(SETTINGS_KEYS.OFFLINE_PAYMENT_TEXT); @@ -37,10 +39,20 @@ const usePayments = () => { } }; + const handleBusinessAccountStorage = async (businessAccounts) => { + const lastBusinessAccountId = await StorageService.get('lastBusinessAccountId'); + if (businessAccounts?.length > 0 && !lastBusinessAccountId) { + await StorageService.save({ lastBusinessAccountId: businessAccounts[0].id }); + } + }; + const loadCustomer = async () => { const customerData = await getCustomer(); setCustomer(customerData); setPaymentMethods(customerData.paymentMethods); + const { businessAccounts } = customerData; + setBusinessPaymentMethods(businessAccounts); + await handleBusinessAccountStorage(businessAccounts); return customerData; }; @@ -48,7 +60,6 @@ const usePayments = () => { if (customer) { return customer; } - return loadCustomer(); }; @@ -184,6 +195,14 @@ const usePayments = () => { } return returnObject; }; + const getBusinessAccountNameById = (id) => { + if (!id) { return null; } + const relevantBusinessAccount = businessPaymentMethods.find(ba => ba.id === id); + if (relevantBusinessAccount) { + return relevantBusinessAccount.name; + } + return null; + }; return { paymentAccount, @@ -193,6 +212,7 @@ const usePayments = () => { loadCustomer, setup, paymentMethods, + businessPaymentMethods, detachPaymentMethod, getOrFetchCustomer, clientHasValidPaymentMethods, @@ -207,6 +227,7 @@ const usePayments = () => { loadOutstandingBalance, offlinePaymentText: offlinePaymentText || i18n.t('payments.offline'), loadOfflinePaymentText, + getBusinessAccountNameById, }; }; diff --git a/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/FutureRides/StopPointRow.js b/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/FutureRides/StopPointRow.js index 0e0ca260b..150885243 100644 --- a/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/FutureRides/StopPointRow.js +++ b/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/FutureRides/StopPointRow.js @@ -1,7 +1,5 @@ import React from 'react'; -import { View } from 'react-native'; import styled from 'styled-components'; -import moment from 'moment'; import i18n from '../../../../I18n'; import Button from '../../../../Components/Button'; diff --git a/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/RideOptions/RideButtons/PaymentButton/index.tsx b/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/RideOptions/RideButtons/PaymentButton/index.tsx index 1d277df1f..fb2739ae4 100644 --- a/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/RideOptions/RideButtons/PaymentButton/index.tsx +++ b/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/RideOptions/RideButtons/PaymentButton/index.tsx @@ -6,6 +6,7 @@ import { PaymentIcon } from 'react-native-payment-icons'; import styled, { ThemeContext } from 'styled-components'; import { useFocusEffect } from '@react-navigation/native'; import SkeletonContent from 'react-native-skeleton-content-nonexpo'; +import BusinessAccountText from '../../../../../../Components/BusinessAccountText'; import { isCardPaymentMethod, isCashPaymentMethod, isOfflinePaymentMethod } from '../../../../../../lib/ride/utils'; import { getCouponText } from '../../../../../../context/newRideContext/utils'; import { MAIN_ROUTES } from '../../../../../routes'; @@ -41,7 +42,7 @@ const CardNameContainer = styled(View)` justify-content: flex-start; flex-direction: row; align-items: center; - width: 55%; + width: ${({ fullWidth }) => (fullWidth ? '100%' : '55%')}; `; const PromoButtonContainer = styled(View)` @@ -76,6 +77,7 @@ interface PaymentButtonProps { brand?: Brand; id?: string; invalid?: boolean; + subTitle?: string; } @@ -85,6 +87,7 @@ const PaymentButton = ({ brand, id, invalid, + subTitle, }: PaymentButtonProps) => { const { primaryColor } = useContext(ThemeContext); const { getCoupon, coupon, setCoupon } = useContext(UserContext); @@ -155,7 +158,7 @@ const PaymentButton = ({ const IconColor = invalid ? '#F83743' : primaryColor; return ( - + {isCardPaymentMethod({ id }) ? : ( )} - {title} + { subTitle + ? + : {title} } {loadPromoButton()} @@ -180,4 +185,5 @@ PaymentButton.defaultProps = { brand: null, id: null, invalid: false, + subTitle: null, }; diff --git a/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/RideOptions/RideButtons/index.tsx b/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/RideOptions/RideButtons/index.tsx index 208f2af3f..0a6ea903b 100644 --- a/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/RideOptions/RideButtons/index.tsx +++ b/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/RideOptions/RideButtons/index.tsx @@ -59,6 +59,8 @@ const RideButtons = ({ defaultService, loadFutureBookingDays, futureBookingDays, + businessAccountId, + serviceEstimations, } = useContext(RidePageContext); @@ -83,9 +85,11 @@ const RideButtons = ({ const [passengersCounterError, setPassengersCounterError] = useState(false); const firstDate = () => moment(ride?.scheduledTo || undefined).add(ride?.scheduledTo ? 0 : (minMinutesBeforeFutureRide || 0) + 1, 'minutes').toDate(); const [tempSelectedDate, setTempSelectedDate] = useState(firstDate()); + const ridePaymentId = ride?.paymentMethodId; + const selectedRequiredFields = chosenService && ridePaymentId && serviceEstimations?.length > 0; + const paymentMethodNotAllowedOnService = !businessAccountId && selectedRequiredFields + && !chosenService.allowedPaymentMethods.includes(getPaymentMethod(ridePaymentId)); - const paymentMethodNotAllowedOnService = chosenService && ride?.paymentMethodId - && !chosenService.allowedPaymentMethods.includes(getPaymentMethod(ride.paymentMethodId)); const checkFutureRidesSetting = async () => { const futureRidesEnabled = await getSettingByKey( @@ -222,7 +226,11 @@ const RideButtons = ({ [PAYMENT_METHODS.OFFLINE]: offlinePaymentMethod, }; const renderPaymentButton = () => { - const { offlinePaymentText, loadOfflinePaymentText } = PaymentsContext.useContainer(); + const { + offlinePaymentText, + getBusinessAccountNameById, + loadOfflinePaymentText, + } = PaymentsContext.useContainer(); useEffect(() => { loadOfflinePaymentText(); }, []); @@ -231,7 +239,11 @@ const RideButtons = ({ PaymentMethodInterface | undefined = paymentMethodIdToDataMap[ridePaymentMethodId] || paymentMethods.find(pm => pm.id === ridePaymentMethodId); - const getSelectedPaymentMethodTitle = () : string => { + const getSelectedPaymentMethodTitle = () : string | null => { + const businessAccountName = getBusinessAccountNameById(businessAccountId); + if (businessAccountName) { + return businessAccountName; + } if (isCashPaymentMethod(selectedPaymentMethod)) { return i18n.t('payments.cash'); } @@ -255,6 +267,7 @@ const RideButtons = ({ brand={selectedPaymentMethod?.brand} icon={paymentMethodToIconMap[selectedPaymentMethod?.id]} title={getSelectedPaymentMethodTitle()} + subTitle={businessAccountId && offlinePaymentText} id={selectedPaymentMethod?.id} invalid={paymentMethodNotAllowedOnService} /> diff --git a/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/RideOptions/ServiceOptions/index.js b/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/RideOptions/ServiceOptions/index.js index 5dc3be9e5..ce10b9057 100644 --- a/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/RideOptions/ServiceOptions/index.js +++ b/examples/client/Locomotion/src/pages/ActiveRide/RideDrawer/RideOptions/ServiceOptions/index.js @@ -1,6 +1,7 @@ import React, { useContext, useEffect } from 'react'; import SkeletonContent from 'react-native-skeleton-content-nonexpo'; import { Text } from 'react-native'; +import EmptyState from '../../../../../Components/EmptyState'; import { getCouponText } from '../../../../../context/newRideContext/utils'; import { RidePageContext } from '../../../../../context/newRideContext'; import { UserContext } from '../../../../../context/user'; @@ -62,13 +63,22 @@ const ServiceOptions = () => { return ( - {(serviceEstimations || []).map(option => ( - - ))} + + { serviceEstimations?.length === 0 + ? ( + + ) + : (serviceEstimations || []).map(option => ( + + )) + } {!isDebuggingEnabled ? ( { stopRequestInterval, serviceEstimations, chosenService, + updateBusinessAccountId, + businessAccountId, } = useContext(RidePageContext); const { @@ -110,6 +113,8 @@ const RideOptions = () => { /> 0} onAddNewMethod={() => { clearPopup(); setTimeout(() => { @@ -118,16 +123,22 @@ const RideOptions = () => { }} showCash={showCash} showOffline={showOffline} - selected={ride?.paymentMethodId - && usePayments.paymentMethods.find((pm: any) => ride.paymentMethodId === pm.id) - ? ride.paymentMethodId : defaultPaymentMethod?.id} + selected={ride?.paymentMethodId || defaultPaymentMethod?.id} rideFlow isVisible={popupToShow === 'payment'} onCancel={() => clearPopup()} onSubmit={(payment: any) => { - updateRidePayload({ - paymentMethodId: payment, - }); + if (payment.isBusiness) { + updateRidePayload({ + paymentMethodId: offlinePaymentMethod.id, + }); + updateBusinessAccountId(payment.id); + } else { + updateBusinessAccountId(null); + updateRidePayload({ + paymentMethodId: payment, + }); + } }} /> diff --git a/examples/client/Locomotion/src/pages/ActiveRide/index.js b/examples/client/Locomotion/src/pages/ActiveRide/index.js index 558577eba..27c477b56 100644 --- a/examples/client/Locomotion/src/pages/ActiveRide/index.js +++ b/examples/client/Locomotion/src/pages/ActiveRide/index.js @@ -118,6 +118,7 @@ const RidePage = ({ mapSettings, navigation }) => { lastSelectedLocation, saveSelectedLocation, reverseLocationGeocode, + updateBusinessAccountId, } = useContext(RidePageContext); const { setIsExpanded, snapPoints, isExpanded, topBarProps, @@ -149,6 +150,7 @@ const RidePage = ({ mapSettings, navigation }) => { setServiceEstimations(null); setChosenService(null); setRide({}); + updateBusinessAccountId(null); changeBsPage(BS_PAGES.ADDRESS_SELECTOR); setSelectedInputIndex(selectedIndex); if (isStationsEnabled) { diff --git a/examples/client/Locomotion/src/pages/Payments/consts.ts b/examples/client/Locomotion/src/pages/Payments/consts.ts index 78cfbdef7..542a67f40 100644 --- a/examples/client/Locomotion/src/pages/Payments/consts.ts +++ b/examples/client/Locomotion/src/pages/Payments/consts.ts @@ -1,12 +1,29 @@ import cashIcon from '../../assets/cash.svg'; import offlineIcon from '../../assets/offline.svg'; +import personalPaymentIcon from '../../assets/personal-payment.svg'; +import businessPaymentIcon from '../../assets/business-payment.svg'; export const PAYMENT_METHODS = { CASH: 'cash', OFFLINE: 'offline', CARD: 'card', }; - +export const PAYMENT_MODES = { + PERSONAL: 'personal', + BUSINESS: 'business', +}; +export const PAYMENT_TABS = [ + { + textKey: 'popups.choosePaymentMethod.tabs.personal', + id: PAYMENT_MODES.PERSONAL, + Svg: personalPaymentIcon, + }, + { + textKey: 'popups.choosePaymentMethod.tabs.business', + id: PAYMENT_MODES.BUSINESS, + Svg: businessPaymentIcon, + }, +]; export const paymentMethodToIconMap = { [PAYMENT_METHODS.CASH]: cashIcon, [PAYMENT_METHODS.OFFLINE]: offlineIcon, diff --git a/examples/client/Locomotion/src/pages/Payments/index.js b/examples/client/Locomotion/src/pages/Payments/index.js index 7808c7326..2eccedabc 100644 --- a/examples/client/Locomotion/src/pages/Payments/index.js +++ b/examples/client/Locomotion/src/pages/Payments/index.js @@ -23,6 +23,7 @@ export default ({ menuSide }) => { } = usePayments; const { updateRidePayload, + updateBusinessAccountId, } = useContext(RidePageContext); const [pageLoading, setPageLoading] = useState(true); @@ -78,6 +79,7 @@ export default ({ menuSide }) => { updateRidePayload({ paymentMethodId, }); + updateBusinessAccountId(null); navigationService.navigate(MAIN_ROUTES.HOME); } else { setShowList(true); diff --git a/examples/client/Locomotion/src/pages/RideHistory/RideCard/index.js b/examples/client/Locomotion/src/pages/RideHistory/RideCard/index.js index ffdfcaf21..e5f69c543 100644 --- a/examples/client/Locomotion/src/pages/RideHistory/RideCard/index.js +++ b/examples/client/Locomotion/src/pages/RideHistory/RideCard/index.js @@ -159,7 +159,6 @@ const RideView = ({ ride }) => { const [isPaymentSuccessPopupVisible, setIsPaymentSuccessPopupVisible] = useState(false); const [outstandingBalance, setOutstandingBalance] = useState(null); const isPaymentRejected = !isPaymentSettled && isRidePaymentRejected; - const usePayments = PaymentContext.useContainer(); const map = createRef(); @@ -260,7 +259,7 @@ const RideView = ({ ride }) => { { const updateRideFromApi = async () => { setLoading(true); - if (params.rideId || ride.id) { - // ts does not recognize the null check - // @ts-ignore - const result = await getRideFromApi(params.rideId || ride.id); + const rideId = params.rideId || ride.id; + if (rideId) { + const result = await getRideFromApi(rideId); setLocalRide(result); setPaymentMethod(result.payment.paymentMethod); @@ -99,7 +98,11 @@ const RidePriceBreakDown = () => { - + { (priceCalculation && isPriceEstimated(priceCalculation.calculationBasis) diff --git a/examples/client/Locomotion/src/popups/ChoosePaymentMethod/index.tsx b/examples/client/Locomotion/src/popups/ChoosePaymentMethod/index.tsx index 9575622a3..2fdb1e2cc 100644 --- a/examples/client/Locomotion/src/popups/ChoosePaymentMethod/index.tsx +++ b/examples/client/Locomotion/src/popups/ChoosePaymentMethod/index.tsx @@ -1,9 +1,12 @@ /* eslint-disable no-unused-expressions */ import React, { useContext, useEffect, useState } from 'react'; -import { View } from 'react-native'; import PropTypes from 'prop-types'; import Modal from 'react-native-modal'; import { useNavigation } from '@react-navigation/native'; +import EmptyState from '../../Components/EmptyState'; +import Mixpanel from '../../services/Mixpanel'; +import { PAYMENT_MODES, PAYMENT_TABS } from '../../pages/Payments/consts'; +import TabSwitch from '../../Components/TabSwitch'; import { getPaymentMethod } from '../../pages/Payments/cardDetailUtils'; import CloseButton from '../../Components/CloseButton'; import i18n from '../../I18n'; @@ -28,12 +31,14 @@ import { MewRidePageContext } from '../../context'; interface PaymentMethodPopupProps { isVisible: boolean; onCancel: () => void; - onSubmit: (payment: string | undefined) => void; + onSubmit: (payment: any) => void; showCash: boolean; rideFlow: boolean; selected: any; onAddNewMethod: () => void; showOffline: boolean; + showBusinessPaymentMethods: boolean; + selectedBusinessAccountId: string | null; } const PaymentMethodPopup = ({ @@ -45,10 +50,22 @@ const PaymentMethodPopup = ({ selected, onAddNewMethod, showOffline, + showBusinessPaymentMethods, + selectedBusinessAccountId, }: PaymentMethodPopupProps) => { - const usePayments: any = PaymentsContext.useContainer(); + const usePayments = PaymentsContext.useContainer(); const { chosenService } = useContext(MewRidePageContext); const [selectedPaymentId, setSelectedPaymentId] = useState(selected); + const [activePaymentTab, setActivePaymentTab] = useState( + selectedBusinessAccountId ? PAYMENT_MODES.BUSINESS : PAYMENT_MODES.PERSONAL, + ); + const isBusinessMode = activePaymentTab === PAYMENT_MODES.BUSINESS; + + const personalPaymentMethods = [ + ...usePayments.paymentMethods, + ...(showCash ? [cashPaymentMethod] : []), + ...(showOffline ? [offlinePaymentMethod] : []), + ]; const getDisabledReason = (paymentMethod: any) => { if ( @@ -61,16 +78,39 @@ const PaymentMethodPopup = ({ }; useEffect(() => { - usePayments.getOrFetchCustomer(); + usePayments.loadCustomer(); }, []); + useEffect(() => { + if (!usePayments.businessPaymentMethods?.length + && activePaymentTab === PAYMENT_MODES.BUSINESS) { + setActivePaymentTab(PAYMENT_MODES.PERSONAL); + } + }, [usePayments.businessPaymentMethods]); + + useEffect(() => { + if (!isVisible) { + setActivePaymentTab( + selectedBusinessAccountId ? PAYMENT_MODES.BUSINESS : PAYMENT_MODES.PERSONAL, + ); + } + }, [isVisible]); + useEffect(() => { const updateDefaultPaymentMethod = async () => { if (selected) { - setSelectedPaymentId(selected); + if (selected === offlinePaymentMethod.id && selectedBusinessAccountId) { + setSelectedPaymentId(selectedBusinessAccountId); + } else { + setSelectedPaymentId(selected); + } } else { const paymentMethod = await usePayments.getClientDefaultMethod(); - setSelectedPaymentId(paymentMethod?.id); + if (paymentMethod?.id) { + setSelectedPaymentId(paymentMethod?.id); + } else if (selectedBusinessAccountId) { + setSelectedPaymentId(selectedBusinessAccountId); + } } }; @@ -78,10 +118,46 @@ const PaymentMethodPopup = ({ updateDefaultPaymentMethod(); }, [usePayments.paymentMethods, selected, chosenService]); - const onSave = (id?: string) => { - onSubmit(id || selectedPaymentId); + const onSave = () => { + const businessPaymentSelected = showBusinessPaymentMethods + && usePayments.businessPaymentMethods.some( + (paymentMethod: any) => paymentMethod.id === selectedPaymentId, + ); + if (businessPaymentSelected) { + onSubmit({ + id: selectedPaymentId, + isBusiness: true, + }); + } else { + onSubmit(selectedPaymentId); + } onCancel(); }; + const renderPersonalPaymentMethods = () => ( + personalPaymentMethods.length > 0 + ? (personalPaymentMethods.map((paymentMethod: any) => { + const reason = getDisabledReason(paymentMethod); + return ( + { + setSelectedPaymentId(paymentMethod.id); + }} + /> + ); + }) + ) : ( + + ) + ); + return ( {i18n.t('popups.choosePaymentMethod.title')} { onCancel(); - setSelectedPaymentId(selected - || (await usePayments.getClientDefaultMethod())?.id); rideFlow ? navigationService.navigate(MAIN_ROUTES.HOME) : navigationService.navigate(MAIN_ROUTES.PAYMENT); }} /> - - - - { - [ - ...usePayments.paymentMethods, - ...(showCash ? [cashPaymentMethod] : []), - ...(showOffline ? [offlinePaymentMethod] : []), - ].map((paymentMethod: any) => { - const reason = getDisabledReason(paymentMethod); - return ( - { - setSelectedPaymentId(paymentMethod.id); - }} - /> - ); - })} + + + { showBusinessPaymentMethods && ( + { + setActivePaymentTab(tab.id); + Mixpanel.setEvent('change payment mode personal / business', { mode: tab.id }); + }} + /> + ) } + + {isBusinessMode ? ( + usePayments.businessPaymentMethods.map((paymentMethod: any) => ( + { + Mixpanel.setEvent('select business payment method', { method: paymentMethod.id }); + setSelectedPaymentId(paymentMethod.id); + }} + /> + )) + ) : renderPersonalPaymentMethods()} + { !isBusinessMode && ( - - - + ) } + +