From a0ac7f76bcd2f50ea1b7a4985775d500db596c66 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Tue, 21 May 2024 22:20:49 +0300 Subject: [PATCH 001/145] open existing group chat if it exists on split expense --- src/libs/ReportUtils.ts | 4 ++-- src/libs/actions/IOU.ts | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b8e4c448fdc2..88b7c642b0ee 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5195,7 +5195,7 @@ function shouldReportBeInOptionList({ /** * Attempts to find a report in onyx with the provided list of participants. Does not include threads, task, expense, room, and policy expense chat. */ -function getChatByParticipants(newParticipantList: number[], reports: OnyxCollection = allReports): OnyxEntry { +function getChatByParticipants(newParticipantList: number[], reports: OnyxCollection = allReports, shouldIncludeGroupChats = false): OnyxEntry { const sortedNewParticipantList = newParticipantList.sort(); return ( Object.values(reports ?? {}).find((report) => { @@ -5209,7 +5209,7 @@ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollec isMoneyRequestReport(report) || isChatRoom(report) || isPolicyExpenseChat(report) || - isGroupChat(report) + (isGroupChat(report) && !shouldIncludeGroupChats) ) { return false; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 2464dbff7dbd..006547044ce7 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3662,11 +3662,9 @@ function getOrCreateOptimisticSplitChatReport(existingSplitChatReportID: string, // Check if the report is available locally if we do have one let existingSplitChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${existingChatReportID}`]; - // If we do not have one locally then we will search for a chat with the same participants (only for 1:1 chats). - const shouldGetOrCreateOneOneDM = participants.length < 2; const allParticipantsAccountIDs = [...participantAccountIDs, currentUserAccountID]; - if (!existingSplitChatReport && shouldGetOrCreateOneOneDM) { - existingSplitChatReport = ReportUtils.getChatByParticipants(allParticipantsAccountIDs); + if (!existingSplitChatReport) { + existingSplitChatReport = ReportUtils.getChatByParticipants(allParticipantsAccountIDs, undefined, participantAccountIDs.length > 1); } // We found an existing chat report we are done... From 6c09eb70483c2b8c3c89e2d29ace8095817a4476 Mon Sep 17 00:00:00 2001 From: James Dean Date: Fri, 31 May 2024 08:51:38 -0700 Subject: [PATCH 002/145] Update en.ts minor copy updates based on the convo here: https://expensify.slack.com/archives/C03U7DCU4/p1717106264594009 --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 8d1b24211583..85800e899821 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1253,7 +1253,7 @@ export default { }, priorityModePage: { priorityMode: 'Priority mode', - explainerText: 'Choose whether to show all chats by default sorted with most recent with pinned items at the top, or #focus on unread pinned items, sorted alphabetically.', + explainerText: 'Choose whether to #focus on unread and pinned chats only, or show everything with the most recent and pinned chats at the top.', priorityModes: { default: { label: 'Most recent', From 233aa5fcf4a4b97b839bb62674e2f2a66028c88a Mon Sep 17 00:00:00 2001 From: James Dean Date: Tue, 4 Jun 2024 09:36:46 -0700 Subject: [PATCH 003/145] Update es.ts --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 29f85edfc93e..4c02590d702d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1252,7 +1252,7 @@ export default { priorityModePage: { priorityMode: 'Modo prioridad', explainerText: - 'Elige si deseas mostrar por defecto todos los chats ordenados desde el más reciente y con los elementos anclados en la parte superior, o elige el modo #concentración, con los elementos no leídos anclados en la parte superior y ordenados alfabéticamente.', + 'Elige #concentración si deseas enfocarte sólo en los chats no leídos y en los anclados, o mostrarlo todo con los chats más recientes y los anclados en la parte superior.', priorityModes: { default: { label: 'Más recientes', From 3a631cf2fe217d23a0e3b7fa3089eb510faa7672 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 12 Jun 2024 11:04:05 +0700 Subject: [PATCH 004/145] fix: a 'new comment' when opening the one expense report for the first time --- src/libs/ReportActionsUtils.ts | 4 +- src/pages/home/report/ReportActionsView.tsx | 125 ++++++++++---------- 2 files changed, 65 insertions(+), 64 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ac0ee9e9025e..df5b20029db9 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -311,8 +311,8 @@ function shouldIgnoreGap(currentReportAction: ReportAction | undefined, nextRepo * Returns a sorted and filtered list of report actions from a report and it's associated child * transaction thread report in order to correctly display reportActions from both reports in the one-transaction report view. */ -function getCombinedReportActions(reportActions: ReportAction[], transactionThreadReportActions: ReportAction[], reportID?: string): ReportAction[] { - if (isEmptyObject(transactionThreadReportActions)) { +function getCombinedReportActions(reportActions: ReportAction[], transactionThreadReportActions: ReportAction[], reportID?: string, isCombineReport?: boolean): ReportAction[] { + if (isEmptyObject(transactionThreadReportActions) && !isCombineReport) { return reportActions; } diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 7084434733aa..8d246c0deb7d 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -96,6 +96,7 @@ function ReportActionsView({ isLoadingNewerReportActions = false, hasLoadingNewerReportActionsError = false, isReadyForCommentLinking = false, + transactionThreadReportID, }: ReportActionsViewProps) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); @@ -154,11 +155,70 @@ function ReportActionsView({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [route, isLoadingInitialReportActions, reportActionID]); + // When we are offline before opening an IOU/Expense report, + // the total of the report and sometimes the expense aren't displayed because these actions aren't returned until `OpenReport` API is complete. + // We generate a fake created action here if it doesn't exist to display the total whenever possible because the total just depends on report data + // and we also generate an expense action if the number of expenses in allReportActions is less than the total number of expenses + // to display at least one expense action to match the total data. + const reportActionsToDisplay = useMemo(() => { + if (!ReportUtils.isMoneyRequestReport(report) || !allReportActions.length) { + return allReportActions; + } + + const actions = [...allReportActions]; + const lastAction = allReportActions[allReportActions.length - 1]; + + if (!ReportActionsUtils.isCreatedAction(lastAction)) { + const optimisticCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(String(report?.ownerAccountID), DateUtils.subtractMillisecondsFromDateTime(lastAction.created, 1)); + optimisticCreatedAction.pendingAction = null; + actions.push(optimisticCreatedAction); + } + + const reportPreviewAction = ReportActionsUtils.getReportPreviewAction(report.chatReportID ?? '', report.reportID); + const moneyRequestActions = allReportActions.filter( + (action) => + action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && + action.originalMessage && + (action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || + !!(action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && action.originalMessage.IOUDetails) || + action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK), + ); + + if (report.total && moneyRequestActions.length < (reportPreviewAction?.childMoneyRequestCount ?? 0) && isEmptyObject(transactionThreadReport)) { + const optimisticIOUAction = ReportUtils.buildOptimisticIOUReportAction( + CONST.IOU.REPORT_ACTION_TYPE.CREATE, + 0, + CONST.CURRENCY.USD, + '', + [], + NumberUtils.rand64(), + undefined, + report.reportID, + false, + false, + {}, + false, + DateUtils.subtractMillisecondsFromDateTime(actions[actions.length - 1].created, 1), + ) as OnyxTypes.ReportAction; + moneyRequestActions.push(optimisticIOUAction); + actions.splice(actions.length - 1, 0, optimisticIOUAction); + } + + // Update pending action of created action if we have some requests that are pending + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const createdAction = actions.pop()!; + if (moneyRequestActions.filter((action) => !!action.pendingAction).length > 0) { + createdAction.pendingAction = CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; + } + + return [...actions, createdAction]; + }, [allReportActions, report, transactionThreadReport]); + // Get a sorted array of reportActions for both the current report and the transaction thread report associated with this report (if there is one) // so that we display transaction-level and report-level report actions in order in the one-transaction view const combinedReportActions = useMemo( - () => ReportActionsUtils.getCombinedReportActions(allReportActions, transactionThreadReportActions), - [allReportActions, transactionThreadReportActions], + () => ReportActionsUtils.getCombinedReportActions(reportActionsToDisplay, transactionThreadReportActions, undefined, !!transactionThreadReportID), + [reportActionsToDisplay, transactionThreadReportActions, transactionThreadReportID], ); const parentReportActionForTransactionThread = useMemo( @@ -460,65 +520,6 @@ function ReportActionsView({ }; }, [isTheFirstReportActionIsLinked]); - // When we are offline before opening an IOU/Expense report, - // the total of the report and sometimes the expense aren't displayed because these actions aren't returned until `OpenReport` API is complete. - // We generate a fake created action here if it doesn't exist to display the total whenever possible because the total just depends on report data - // and we also generate an expense action if the number of expenses in reportActions is less than the total number of expenses - // to display at least one expense action to match the total data. - const reportActionsToDisplay = useMemo(() => { - if (!ReportUtils.isMoneyRequestReport(report) || !reportActions.length) { - return reportActions; - } - - const actions = [...reportActions]; - const lastAction = reportActions[reportActions.length - 1]; - - if (!ReportActionsUtils.isCreatedAction(lastAction)) { - const optimisticCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(String(report?.ownerAccountID), DateUtils.subtractMillisecondsFromDateTime(lastAction.created, 1)); - optimisticCreatedAction.pendingAction = null; - actions.push(optimisticCreatedAction); - } - - const reportPreviewAction = ReportActionsUtils.getReportPreviewAction(report.chatReportID ?? '', report.reportID); - const moneyRequestActions = reportActions.filter( - (action) => - action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - action.originalMessage && - (action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || - !!(action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && action.originalMessage.IOUDetails) || - action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK), - ); - - if (report.total && moneyRequestActions.length < (reportPreviewAction?.childMoneyRequestCount ?? 0) && isEmptyObject(transactionThreadReport)) { - const optimisticIOUAction = ReportUtils.buildOptimisticIOUReportAction( - CONST.IOU.REPORT_ACTION_TYPE.CREATE, - 0, - CONST.CURRENCY.USD, - '', - [], - NumberUtils.rand64(), - undefined, - report.reportID, - false, - false, - {}, - false, - DateUtils.subtractMillisecondsFromDateTime(actions[actions.length - 1].created, 1), - ) as OnyxTypes.ReportAction; - moneyRequestActions.push(optimisticIOUAction); - actions.splice(actions.length - 1, 0, optimisticIOUAction); - } - - // Update pending action of created action if we have some requests that are pending - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const createdAction = actions.pop()!; - if (moneyRequestActions.filter((action) => !!action.pendingAction).length > 0) { - createdAction.pendingAction = CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; - } - - return [...actions, createdAction]; - }, [reportActions, report, transactionThreadReport]); - // Comments have not loaded at all yet do nothing if (!reportActions.length) { return null; @@ -534,7 +535,7 @@ function ReportActionsView({ parentReportAction={parentReportAction} parentReportActionForTransactionThread={parentReportActionForTransactionThread} onLayout={recordTimeToMeasureItemLayout} - sortedReportActions={reportActionsToDisplay} + sortedReportActions={reportActions} mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} From 61859a628652201881635cdcc890cc8d86b71cea Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Wed, 12 Jun 2024 16:24:47 +0300 Subject: [PATCH 005/145] Add billing currency --- src/CONST.ts | 1 + src/ONYXKEYS.ts | 3 + src/ROUTES.ts | 2 + src/SCREENS.ts | 2 + .../PaymentCardChangeCurrencyForm.tsx | 113 ++++++++++++++++++ .../AddPaymentCard/PaymentCardForm.tsx | 33 ++--- src/languages/en.ts | 11 ++ src/languages/es.ts | 11 ++ .../parameters/UpdateBillingCurrencyParams.ts | 9 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + .../ModalStackNavigators/index.tsx | 3 + .../CENTRAL_PANE_TO_RHP_MAPPING.ts | 7 +- .../FULL_SCREEN_TO_RHP_MAPPING.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 8 ++ src/libs/Navigation/types.ts | 1 + src/libs/actions/PaymentMethods.ts | 70 ++++++++++- .../PaymentCard/ChangeCurrency/index.tsx | 44 +++++++ .../CardSection/CardSectionActions/index.tsx | 4 +- .../CardSectionDataEmpty/index.tsx | 10 +- .../ChangeBillingCurrency/index.tsx | 45 +++++++ src/types/form/AddDebitCardForm.ts | 3 + src/types/form/ChangeBillingCurrencyForm.ts | 20 ++++ src/types/form/index.ts | 1 + src/types/onyx/Fund.ts | 3 +- 25 files changed, 379 insertions(+), 29 deletions(-) create mode 100644 src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx create mode 100644 src/libs/API/parameters/UpdateBillingCurrencyParams.ts create mode 100644 src/pages/settings/PaymentCard/ChangeCurrency/index.tsx create mode 100644 src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx create mode 100644 src/types/form/ChangeBillingCurrencyForm.ts diff --git a/src/CONST.ts b/src/CONST.ts index b0e3ab8c3af4..c26d9cbf21de 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -590,6 +590,7 @@ const CONST = { ONFIDO_TERMS_OF_SERVICE_URL: 'https://onfido.com/terms-of-service/', LIST_OF_RESTRICTED_BUSINESSES: 'https://community.expensify.com/discussion/6191/list-of-restricted-businesses', TRAVEL_TERMS_URL: `${USE_EXPENSIFY_URL}/travelterms`, + PRICING: `https://www.expensify.com/pricing`, // Use Environment.getEnvironmentURL to get the complete URL with port number DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0d22d3714fe6..084a1fbccd07 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -450,6 +450,8 @@ const ONYXKEYS = { SETTINGS_STATUS_SET_CLEAR_AFTER_FORM_DRAFT: 'settingsStatusSetClearAfterFormDraft', SETTINGS_STATUS_CLEAR_DATE_FORM: 'settingsStatusClearDateForm', SETTINGS_STATUS_CLEAR_DATE_FORM_DRAFT: 'settingsStatusClearDateFormDraft', + CHANGE_BILLING_CURRENCY_FORM: 'changeBillingCurrencyForm', + CHANGE_BILLING_CURRENCY_FORM_DRAFT: 'changeBillingCurrencyFormDraft', PRIVATE_NOTES_FORM: 'privateNotesForm', PRIVATE_NOTES_FORM_DRAFT: 'privateNotesFormDraft', I_KNOW_A_TEACHER_FORM: 'iKnowTeacherForm', @@ -524,6 +526,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WAYPOINT_FORM]: FormTypes.WaypointForm; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: FormTypes.SettingsStatusSetForm; [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM]: FormTypes.SettingsStatusClearDateForm; + [ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM]: FormTypes.ChangeBillingCurrencyForm; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM]: FormTypes.SettingsStatusSetClearAfterForm; [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: FormTypes.PrivateNotesForm; [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM]: FormTypes.IKnowTeacherForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index ed20d388bb87..85a320ac2e06 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -95,6 +95,7 @@ const ROUTES = { WORKSPACE_SWITCHER: 'workspace-switcher', SETTINGS: 'settings', SETTINGS_PROFILE: 'settings/profile', + SETTINGS_CHANGE_CURRENCY: 'settings/add-payment-card/change-currency', SETTINGS_SHARE_CODE: 'settings/shareCode', SETTINGS_DISPLAY_NAME: 'settings/profile/display-name', SETTINGS_TIMEZONE: 'settings/profile/timezone', @@ -104,6 +105,7 @@ const ROUTES = { SETTINGS_SUBSCRIPTION: 'settings/subscription', SETTINGS_SUBSCRIPTION_SIZE: 'settings/subscription/subscription-size', SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD: 'settings/subscription/add-payment-card', + SETTINGS_SUBSCRIPTION_CHANGE_BILLING_CURRENCY: 'settings/subscription/change-billing-currency', SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY: 'settings/subscription/disable-auto-renew-survey', SETTINGS_PRIORITY_MODE: 'settings/preferences/priority-mode', SETTINGS_LANGUAGE: 'settings/preferences/language', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9f7277a0ad0f..a8e2023f5cd9 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -41,6 +41,7 @@ const SCREENS = { SAVE_THE_WORLD: 'Settings_TeachersUnite', APP_DOWNLOAD_LINKS: 'Settings_App_Download_Links', ADD_DEBIT_CARD: 'Settings_Add_Debit_Card', + ADD_PAYMENT_CARD_CHANGE_CURRENCY: 'Settings_Add_Payment_Card_Change_Currency', ADD_BANK_ACCOUNT: 'Settings_Add_Bank_Account', ADD_BANK_ACCOUNT_REFACTOR: 'Settings_Add_Bank_Account_Refactor', CLOSE: 'Settings_Close', @@ -109,6 +110,7 @@ const SCREENS = { SIZE: 'Settings_Subscription_Size', ADD_PAYMENT_CARD: 'Settings_Subscription_Add_Payment_Card', DISABLE_AUTO_RENEW_SURVEY: 'Settings_Subscription_DisableAutoRenewSurvey', + CHANGE_BILLING_CURRENCY: 'Settings_Subscription_Change_Billing_Currency', }, }, SAVE_THE_WORLD: { diff --git a/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx b/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx new file mode 100644 index 000000000000..d0c291225802 --- /dev/null +++ b/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx @@ -0,0 +1,113 @@ +import React, {useCallback, useState} from 'react'; +import type {ValueOf} from 'type-fest'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import Hoverable from '@components/Hoverable'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/ChangeBillingCurrencyForm'; +import PaymentCardCurrencyModal from './PaymentCardCurrencyModal'; + +type PaymentCardFormProps = { + initialCurrency?: ValueOf; + isSecurityCodeRequired?: boolean; + changeBillingCurrency: (currency?: ValueOf, values?: FormOnyxValues) => void; +}; + +const REQUIRED_FIELDS = [INPUT_IDS.SECURITY_CODE]; + +function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeRequired, initialCurrency}: PaymentCardFormProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const [isCurrencyModalVisible, setIsCurrencyModalVisible] = useState(false); + const [currency, setCurrency] = useState>(initialCurrency ?? CONST.CURRENCY.USD); + + const validate = (formValues: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(formValues, REQUIRED_FIELDS); + + if (formValues.securityCode && !ValidationUtils.isValidSecurityCode(formValues.securityCode)) { + errors.securityCode = 'billingCurrency.error.securityCode'; + } + + return errors; + }; + + const showCurrenciesModal = useCallback(() => { + setIsCurrencyModalVisible(true); + }, []); + + const changeCurrency = useCallback((newCurrency: keyof typeof CONST.CURRENCY) => { + setCurrency(newCurrency); + setIsCurrencyModalVisible(false); + }, []); + + return ( + changeBillingCurrency(currency, formData)} + submitButtonText={translate('common.save')} + scrollContextEnabled + style={[styles.mh5, styles.flexGrow1]} + > + + {(isHovered) => ( + + )} + + + {!!isSecurityCodeRequired && ( + + )} + + } + currentCurrency={currency} + onCurrencyChange={changeCurrency} + onClose={() => setIsCurrencyModalVisible(false)} + /> + + {`${translate('billingCurrency.note')}`}{' '} + {`${translate('billingCurrency.noteLink')}`}{' '} + {`${translate('billingCurrency.notDetails')}`} + + + ); +} + +PaymentCardChangeCurrencyForm.displayName = 'PaymentCardChangeCurrencyForm'; + +export default PaymentCardChangeCurrencyForm; diff --git a/src/components/AddPaymentCard/PaymentCardForm.tsx b/src/components/AddPaymentCard/PaymentCardForm.tsx index 61e9a2d1860a..5fcf3c2dd6cf 100644 --- a/src/components/AddPaymentCard/PaymentCardForm.tsx +++ b/src/components/AddPaymentCard/PaymentCardForm.tsx @@ -1,7 +1,8 @@ import {useRoute} from '@react-navigation/native'; -import React, {useCallback, useRef, useState} from 'react'; +import React, {useCallback, useRef} from 'react'; import type {ReactNode} from 'react'; import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import AddressSearch from '@components/AddressSearch'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; @@ -18,13 +19,13 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ValidationUtils from '@libs/ValidationUtils'; +import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/AddDebitCardForm'; -import PaymentCardCurrencyModal from './PaymentCardCurrencyModal'; type PaymentCardFormProps = { shouldShowPaymentCardForm?: boolean; @@ -129,15 +130,14 @@ function PaymentCardForm({ headerContent, }: PaymentCardFormProps) { const styles = useThemeStyles(); + const [data] = useOnyx(ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM); + const {translate} = useLocalize(); const route = useRoute(); const label = CARD_LABELS[isDebitCard ? CARD_TYPES.DEBIT_CARD : CARD_TYPES.PAYMENT_CARD]; const cardNumberRef = useRef(null); - const [isCurrencyModalVisible, setIsCurrencyModalVisible] = useState(false); - const [currency, setCurrency] = useState(CONST.CURRENCY.USD); - const validate = (formValues: FormOnyxValues): FormInputErrors => { const errors = ValidationUtils.getFieldRequiredErrors(formValues, REQUIRED_FIELDS); @@ -172,13 +172,8 @@ function PaymentCardForm({ return errors; }; - const showCurrenciesModal = useCallback(() => { - setIsCurrencyModalVisible(true); - }, []); - - const changeCurrency = useCallback((newCurrency: keyof typeof CONST.CURRENCY) => { - setCurrency(newCurrency); - setIsCurrencyModalVisible(false); + const openCurrenciesSelectScreen = useCallback(() => { + Navigation.navigate(ROUTES.SETTINGS_CHANGE_CURRENCY); }, []); if (!shouldShowPaymentCardForm) { @@ -191,7 +186,7 @@ function PaymentCardForm({ addPaymentCard(formData, currency)} + onSubmit={addPaymentCard} submitButtonText={submitButtonText} scrollContextEnabled style={[styles.mh5, styles.flexGrow1]} @@ -279,8 +274,8 @@ function PaymentCardForm({ aria-label={translate('common.currency')} role={CONST.ROLE.COMBOBOX} icon={Expensicons.ArrowRight} - onPress={showCurrenciesModal} - value={currency} + onPress={openCurrenciesSelectScreen} + value={data?.currency ?? CONST.CURRENCY.USD} containerStyles={[styles.mt5]} inputStyle={isHovered && styles.cursorPointer} hideFocusedState @@ -302,14 +297,6 @@ function PaymentCardForm({ /> )} - - } - currentCurrency={currency} - onCurrencyChange={changeCurrency} - onClose={() => setIsCurrencyModalVisible(false)} - /> {footerContent} diff --git a/src/languages/en.ts b/src/languages/en.ts index 76e4d5d5a143..5d104eebb189 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1051,6 +1051,17 @@ export default { genericFailureMessage: "Private notes couldn't be saved.", }, }, + billingCurrency: { + error: { + securityCode: 'Please enter a valid security code.', + }, + securityCode: 'Security code', + changePaymentCurrency: 'Change payment currency', + paymentCurrency: 'Payment currency', + note: 'Note: Changing your payment currency can impact how much you’ll pay for Expensify. Refer to our', + noteLink: 'pricing page', + notDetails: 'for full details.', + }, addDebitCardPage: { addADebitCard: 'Add a debit card', nameOnCard: 'Name on card', diff --git a/src/languages/es.ts b/src/languages/es.ts index d03e13d1a9ff..69b7d6c78b29 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1048,6 +1048,17 @@ export default { genericFailureMessage: 'Las notas privadas no han podido ser guardadas.', }, }, + billingCurrency: { + error: { + securityCode: 'Por favor, introduce un código de seguridad válido.', + }, + securityCode: 'Código de seguridad', + changePaymentCurrency: 'Cambiar la moneda de pago', + paymentCurrency: 'Moneda de pago', + note: 'Nota: Cambiar tu moneda de pago puede afectar cuánto pagarás por Expensify. Consulta nuestra', + noteLink: 'página de precios', + notDetails: 'para conocer todos los detalles.', + }, addDebitCardPage: { addADebitCard: 'Añadir una tarjeta de débito', nameOnCard: 'Nombre en la tarjeta', diff --git a/src/libs/API/parameters/UpdateBillingCurrencyParams.ts b/src/libs/API/parameters/UpdateBillingCurrencyParams.ts new file mode 100644 index 000000000000..957b377a2e95 --- /dev/null +++ b/src/libs/API/parameters/UpdateBillingCurrencyParams.ts @@ -0,0 +1,9 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type UpdateBillingCurrencyParams = { + currency: ValueOf; + cardCVV: string; +}; + +export default UpdateBillingCurrencyParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index b853f134b315..17d73538f58d 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -97,6 +97,7 @@ export type {default as UpdateRoomVisibilityParams} from './UpdateRoomVisibility export type {default as UpdateReportWriteCapabilityParams} from './UpdateReportWriteCapabilityParams'; export type {default as AddWorkspaceRoomParams} from './AddWorkspaceRoomParams'; export type {default as UpdatePolicyRoomNameParams} from './UpdatePolicyRoomNameParams'; +export type {default as UpdateBillingCurrencyParams} from './UpdateBillingCurrencyParams'; export type {default as AddEmojiReactionParams} from './AddEmojiReactionParams'; export type {default as RemoveEmojiReactionParams} from './RemoveEmojiReactionParams'; export type {default as LeaveRoomParams} from './LeaveRoomParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 6b34e04e1937..8eafacda915c 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -159,6 +159,7 @@ const WRITE_COMMANDS = { UPDATE_MONEY_REQUEST_DESCRIPTION: 'UpdateMoneyRequestDescription', UPDATE_MONEY_REQUEST_AMOUNT_AND_CURRENCY: 'UpdateMoneyRequestAmountAndCurrency', HOLD_MONEY_REQUEST: 'HoldRequest', + UPDATE_BILLING_CARD_CURRENCY: 'UpdateBillingCardCurrency', UNHOLD_MONEY_REQUEST: 'UnHoldRequest', UPDATE_DISTANCE_REQUEST: 'UpdateDistanceRequest', REQUEST_MONEY: 'RequestMoney', @@ -435,6 +436,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED]: Parameters.SetPolicyDistanceRatesEnabledParams; [WRITE_COMMANDS.DELETE_POLICY_DISTANCE_RATES]: Parameters.DeletePolicyDistanceRatesParams; [WRITE_COMMANDS.DISMISS_TRACK_EXPENSE_ACTIONABLE_WHISPER]: Parameters.DismissTrackExpenseActionableWhisperParams; + [WRITE_COMMANDS.UPDATE_BILLING_CARD_CURRENCY]: Parameters.UpdateBillingCurrencyParams; [WRITE_COMMANDS.CONVERT_TRACKED_EXPENSE_TO_REQUEST]: Parameters.ConvertTrackedExpenseToRequestParams; [WRITE_COMMANDS.CATEGORIZE_TRACKED_EXPENSE]: Parameters.CategorizeTrackedExpenseParams; [WRITE_COMMANDS.SHARE_TRACKED_EXPENSE]: Parameters.ShareTrackedExpenseParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 807c938e21dd..6b2dd3a9ef2c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -29,6 +29,7 @@ import type { TravelNavigatorParamList, WalletStatementNavigatorParamList, } from '@navigation/types'; +import ChangeCurrency from '@pages/settings/PaymentCard/ChangeCurrency'; import type {ThemeStyles} from '@styles/index'; import type {Screen} from '@src/SCREENS'; import SCREENS from '@src/SCREENS'; @@ -331,6 +332,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/taxes/WorkspaceCreateTaxPage').default as React.ComponentType, [SCREENS.SETTINGS.SAVE_THE_WORLD]: () => require('../../../../pages/TeachersUnite/SaveTheWorldPage').default as React.ComponentType, [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: () => require('../../../../pages/settings/Subscription/PaymentCard/AddPaymentCard').default as React.ComponentType, + [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: () => require('../../../../pages/settings/Subscription/PaymentCard/ChangeBillingCurrency').default as React.ComponentType, + [SCREENS.SETTINGS.ADD_PAYMENT_CARD_CHANGE_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default as React.ComponentType, }); const EnablePaymentsStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index c4858d3141f1..2402dd01e00c 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -39,7 +39,12 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = [SCREENS.SETTINGS.SAVE_THE_WORLD]: [SCREENS.I_KNOW_A_TEACHER, SCREENS.INTRO_SCHOOL_PRINCIPAL, SCREENS.I_AM_A_TEACHER], [SCREENS.SETTINGS.TROUBLESHOOT]: [SCREENS.SETTINGS.CONSOLE], [SCREENS.SEARCH.CENTRAL_PANE]: [SCREENS.SEARCH.REPORT_RHP], - [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD, SCREENS.SETTINGS.SUBSCRIPTION.SIZE, SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY], + [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: [ + SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD, + SCREENS.SETTINGS.SUBSCRIPTION.SIZE, + SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY, + SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY, + ], }; export default CENTRAL_PANE_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index f91d290639ff..ab8647c5b8ff 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -11,6 +11,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.OWNER_CHANGE_CHECK, SCREENS.WORKSPACE.OWNER_CHANGE_SUCCESS, SCREENS.WORKSPACE.OWNER_CHANGE_ERROR, + SCREENS.WORKSPACE.OWNER_CHANGE_ERROR, ], [SCREENS.WORKSPACE.WORKFLOWS]: [ SCREENS.WORKSPACE.WORKFLOWS_APPROVER, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index bb002ec2c01f..a2b7da387112 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -130,6 +130,14 @@ const config: LinkingOptions['config'] = { path: ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD, exact: true, }, + [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: { + path: ROUTES.SETTINGS_SUBSCRIPTION_CHANGE_BILLING_CURRENCY, + exact: true, + }, + [SCREENS.SETTINGS.ADD_PAYMENT_CARD_CHANGE_CURRENCY]: { + path: ROUTES.SETTINGS_CHANGE_CURRENCY, + exact: true, + }, [SCREENS.SETTINGS.PREFERENCES.THEME]: { path: ROUTES.SETTINGS_THEME, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 5597e7ce00da..c100a9355b03 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -250,6 +250,7 @@ type SettingsNavigatorParamList = { tagName: string; }; [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: undefined; + [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: undefined; [SCREENS.WORKSPACE.TAXES_SETTINGS]: { policyID: string; }; diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index c12f7a042659..f4c330711ae6 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -5,7 +5,14 @@ import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; -import type {AddPaymentCardParams, DeletePaymentCardParams, MakeDefaultPaymentMethodParams, PaymentCardParams, TransferWalletBalanceParams} from '@libs/API/parameters'; +import { + AddPaymentCardParams, + DeletePaymentCardParams, + MakeDefaultPaymentMethodParams, + PaymentCardParams, + TransferWalletBalanceParams, + UpdateBillingCurrencyParams, +} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CardUtils from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -273,6 +280,17 @@ function clearDebitCardFormErrorAndSubmit() { [INPUT_IDS.ADDRESS_ZIP_CODE]: '', [INPUT_IDS.ADDRESS_STATE]: '', [INPUT_IDS.ACCEPT_TERMS]: '', + [INPUT_IDS.CURRENCY]: CONST.CURRENCY.USD, + }); +} + +/** + * Set currency for payments + * + */ +function setPaymentMethodCurrency(currency: ValueOf) { + Onyx.merge(ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, { + [INPUT_IDS.CURRENCY]: currency, }); } @@ -425,6 +443,54 @@ function deletePaymentCard(fundID: number) { }); } +/** + * Call the API to change billing currency. + * + */ +function updateBillingCurrency(currency: ValueOf, cardCVV: string) { + const parameters: UpdateBillingCurrencyParams = { + cardCVV, + currency, + }; + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: 'merge', + key: ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM, + value: { + isLoading: true, + errors: null, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: 'merge', + key: ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM, + value: { + isLoading: false, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: 'merge', + key: ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM, + value: { + isLoading: false, + }, + }, + ]; + + API.write(WRITE_COMMANDS.UPDATE_BILLING_CARD_CURRENCY, parameters, { + optimisticData, + successData, + failureData, + }); +} + export { deletePaymentCard, addPaymentCard, @@ -440,8 +506,10 @@ export { saveWalletTransferAccountTypeAndID, saveWalletTransferMethodType, hasPaymentMethodError, + updateBillingCurrency, clearDeletePaymentMethodError, clearAddPaymentMethodError, clearWalletError, + setPaymentMethodCurrency, clearWalletTermsError, }; diff --git a/src/pages/settings/PaymentCard/ChangeCurrency/index.tsx b/src/pages/settings/PaymentCard/ChangeCurrency/index.tsx new file mode 100644 index 000000000000..d57bdda4cdfb --- /dev/null +++ b/src/pages/settings/PaymentCard/ChangeCurrency/index.tsx @@ -0,0 +1,44 @@ +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import PaymentCardChangeCurrencyForm from '@components/AddPaymentCard/PaymentCardChangeCurrencyForm'; +import type {FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@navigation/Navigation'; +import * as PaymentMethods from '@userActions/PaymentMethods'; +import type CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +function ChangeCurrency() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [debitCardForm] = useOnyx(ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM); + + const changeCurrency = useCallback((currency?: ValueOf) => { + if (currency) { + PaymentMethods.setPaymentMethodCurrency(currency); + } + + Navigation.goBack(); + }, []); + + return ( + + + + + + + ); +} + +ChangeCurrency.displayName = 'ChangeCurrency'; + +export default ChangeCurrency; diff --git a/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx b/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx index eb40977c25a9..0347d88abf52 100644 --- a/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx @@ -5,8 +5,10 @@ import ThreeDotsMenu from '@components/ThreeDotsMenu'; import type ThreeDotsMenuProps from '@components/ThreeDotsMenu/types'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import Navigation from '@navigation/Navigation'; import type {AnchorPosition} from '@styles/index'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; const anchorAlignment = { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, @@ -29,7 +31,7 @@ function CardSectionActions() { { icon: Expensicons.MoneyCircle, text: translate('subscription.cardSection.changeCurrency'), - onSelected: () => {}, // TODO: update with navigation to "change currency" screen (https://github.com/Expensify/App/issues/38621) + onSelected: () => Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_CHANGE_BILLING_CURRENCY), }, ], [translate], diff --git a/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.tsx b/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.tsx index 13131841c53d..9f51718f0fee 100644 --- a/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.tsx @@ -1,16 +1,22 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import Button from '@components/Button'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@navigation/Navigation'; +import ROUTES from '@src/ROUTES'; function CardSectionDataEmpty() { const {translate} = useLocalize(); const styles = useThemeStyles(); + const openAddPaymentCardScreen = useCallback(() => { + Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD); + }, []); + return (