From 6e131d082b462ce6ba8b9083f66a746c57f8614b Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Fri, 30 Aug 2024 12:21:06 -0500 Subject: [PATCH 001/273] Update and rename Additional-Travel-Integrations.md to Travel-receipt-integrations.md Article updates to include content from community --- .../Additional-Travel-Integrations.md | 73 ----------- .../Travel-receipt-integrations.md | 121 ++++++++++++++++++ 2 files changed, 121 insertions(+), 73 deletions(-) delete mode 100644 docs/articles/expensify-classic/connections/Additional-Travel-Integrations.md create mode 100644 docs/articles/expensify-classic/connections/Travel-receipt-integrations.md diff --git a/docs/articles/expensify-classic/connections/Additional-Travel-Integrations.md b/docs/articles/expensify-classic/connections/Additional-Travel-Integrations.md deleted file mode 100644 index 7dcc8e5e9c29..000000000000 --- a/docs/articles/expensify-classic/connections/Additional-Travel-Integrations.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: Importing Receipts from Various Platforms to Expensify -description: Detailed guide on how to import receipts from multiple travel platforms into Expensify. ---- - -# Overview -You can automatically import receipts from many travel platforms into Expensify, to make tracking expenses while traveling for business a breeze. Read on to learn how to import receipts from Bolt Work, Spot Hero, Trainline, Grab, HotelTonight, and Kayak for Business. - -## How to Connect to Bolt Work - -### Set Up Bolt Work Profile -- Open the Bolt app, go to the side navigation menu, and select Payment. -- At the bottom, select Set up work profile and follow the instructions, entering your work email for verification. - -### Link to Expensify -- In the Bolt app, go to Work Rides. -- Select Add expense provider, choose Expensify, and enter the associated email to receive a verification link. -- Ensure you select your work ride profile as the payment method before booking. - -## How to Connect to SpotHero - -### Set up a Business Profile -- Open the SpotHero app, click the hamburger icon, and go to Account Settings. -- Click Set up Business Profile. -- Specify the email connected to Expensify and set up your payment method. -- Upon checkout, choose between Business and Personal Profiles in the "Payment Details" section. -- If you want, you can set a weekly or monthly cadence for consolidated SpotHero expense reports in your Business Profile settings. This will batch all of your SpotHero expenses to import into Expensify at that cadence. - -## How to Connect to Trainline -- To send a ticket receipt to Expensify: - - In the Trainline app, navigate to the My Tickets tab. - - Tap Manage my booking > Expense receipt > Send to Expensify. -- That’s it! - -## How to Connect to Grab -- In the Grab app, tap on your name, go to “Profiles”, and “Add a business profile”. -- Follow instructions and enter your work email for verification. -- In your profile, tap on Business > Expense Solution > Expensify > Save. -- Before booking, select your Business profile and confirm. - -## How to Connect to HotelTonight -- In HotelTonight, go to the Bookings tab and select your booking. -- Select Receipt > Expensify, enter your Expensify email, and send. - -## How to Connect to Kayak for Business - -### Admin Setup -- Admins should go to “Company Settings” and click on “Connect to Expensify”. -- Bookings made by employees will automatically be sent to Expensify. - -### Traveler Setup -- From your account settings, choose whether expenses should be sent to Expensify automatically or manually. -- We recommend sending them automatically, so you can travel without even thinking about your expense reports. - -{% include faq-begin.md %} - -**Q: What if I don’t have the option for Send to Expensify in Trainline?** - -A: This can happen if the native iOS Mail app is not installed on an Apple device. However, you can still use the native iOS share to Expensify function for Trainline receipts. - -**Q: Why should I choose automatic mode in Kayak for Business?** - -A: Automatic mode is less effort as it’s easier to delete an expense in Expensify than to remember to forward a forgotten receipt. - -**Q: Can I receive consolidated reports from SpotHero?** - -A: Yes, you can set a weekly or monthly cadence for SpotHero expenses to be emailed in a consolidated report. - -**Q: Do I need to select a specific profile before booking in Bolt Work and Grab?** - -A: Yes, ensure you have selected your work or business profile as the payment method before booking. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/connections/Travel-receipt-integrations.md b/docs/articles/expensify-classic/connections/Travel-receipt-integrations.md new file mode 100644 index 000000000000..bf0d7e05997d --- /dev/null +++ b/docs/articles/expensify-classic/connections/Travel-receipt-integrations.md @@ -0,0 +1,121 @@ +--- +title: Travel Receipt Integrations +description: How to use pre-built or custom integrations to track travel expenses +--- + +Expensify’s receipt integrations allow a merchant to upload receipts directly to a user’s Expensify account. A merchant just has to email a receipt to an Expensify user and Cc receipts@expensify.com. This automatically creates a transaction in the Expensify account for the user whose email address is in the To field. + +You can set up a receipt integration by using one of our existing pre-built integrations, or by building your own receipt integration. + +## Use a pre-built travel integration + +You can use our pre-built integrations to automatically import travel receipts from Bolt Work, Spot Hero, Grab, and Kayak for Business. + +### Bolt Work + +1. In the Bolt app, tap the menu icon in the top left and tap **Work trips**. +2. Tap **Create profile**. +3. Enter the email address that you use for Expensify, then tap **Next**. +4. Enter your company details, then tap **Next**. +5. Choose a payment method. If you don’t want to use the existing payment methods, you can create a new one by tapping **Add Payment Method**. Then tap **Next**. +6. Tap **Done**. +7. Tap Add expense provider, then tap **Expensify**. +8. Tap **Verify**. +9. Tap the menu icon on the top left and tap **Work trips** once more. +10. Tap **Add expense provider** and select **Expensify** again. + +When booking a trip with Bolt Work, select your work trip profile as the payment method before booking. Then the receipt details will be automatically sent to Expensify. + +### SpotHero + +1. In the SpotHero app, tap the menu icon in the top left and tap **Account Settings**. +2. Tap **Set up Business Profile**. +3. Tap **Create Business Profile**. +4. Enter the email address you use for Expensify and tap **Next**. +5. Tap **Add a Payment Method** and enter your payment account details. Then tap **Next**. +6. Tap **Expensify**. + +When reserving parking with SpotHero, select your business profile in the Payment Details section. Then the receipt will be automatically sent to Expensify. In your SpotHero Business Profile settings, you can also set a weekly or monthly cadence for SpotHero to send a batch of expenses to Expensify. + +### Grab + +1. In the Grab app, tap your profile picture in the top left. +2. Tap your user icon again at the top of the settings menu. +3. Tap **Add a business profile**. +4. Tap Next twice, then tap **Let’s Get Started**. +5. Enter the email address you use for Expensify and tap the next arrow in the bottom right. +6. Check your email and copy the verification code you receive from Grab. +7. Tap **Manage My Business Profile**. +8. Under Preferences, tap **Expense Solution**. +9. Tap **Expensify**, then tap **Save**. + +When booking a trip with Grab, tap **personal** and select **business** to ensure your business profile is selected. Then the receipt will be automatically sent to Expensify. + +### KAYAK for Business + +**Admin Setup** + +This process must be completed by a KAYAK for Business admin. + +1. On your KAYAK for Business homepage, click **Company Settings**. +2. Click **Connect to Expensify**. + +KAYAK for Business will now forward bookings made by each employee into Expensify. + +**Traveler Setup** + +1. On your KAYAK for Business homepage, click **Profile Account Settings**. +2. Enable the Expensify toggle to have your expenses automatically sent to Expensify. You also have the option to send them manually. + +## Build your own receipt integration + +1. Email receiptintegration@expensify.com and include: + - **Subject**: Use “Receipt Integration Request" as the subject line + - **Body**: List all email addresses the merchant sends email receipts from +2. Once you receive your email confirmation (within approximately 2 weeks) that the email addresses have been whitelisted, you’ll then be able to Cc “receipts@expensify.com” on receipt emails to users, and transactions will be created in the users’ Expensify account. +3. Test the integration by sending a receipt email to the email address you used to create your Expensify account and Cc “receipts@expensify.com”. Wait for the receipt to be SmartScanned. Then you will see the merchant, date, and amount added to the transaction. + +### Using the integration + +When sending an emailed receipt: + +- Attachments on an email (that are not an .ics file) will be SmartScanned. We recommend including the receipt as the only attachment. +- You can only include one email address in the To field. In the Cc field, include only receipts@expensify.com. +- Reservations for hotels and car rentals cannot be sent to Expensify as an expense because they are paid at the end of usage. You can only send transaction data for purchases that have already been made. +- Use standardized three-letter currency codes (ISO 4217) where applicable. + +{% include faq-begin.md %} + +**In Trainline, what if I don’t have the option for Send to Expensify?** + +This can happen if the native iOS Mail app is not installed on an Apple device. However, you can still use the native iOS Share to Expensify function for Trainline receipts. + +**Why does it take 2 weeks to set up a custom integration?** + +Receipt integrations require our engineers to manually set them up on the backend. For that reason, it can take up to 2 weeks to set it up. + +**Is there a way to connect via API?** + +No, at this time there are no API receipt integrations. All receipt integrations are managed via receipt emails. + +**What is your Open API?** + +Our Open API is a self-serve tool meant to pull information out of Expensify. Typically, this tool is used to build integrations with accounting solutions that we don’t directly integrate with. If you wish to push data into Expensify, the only way to integrate is via the receipt integration options listed above in this article. + +**Are you able to split one email into separate receipts?** + +The receipt integration is unable to automatically split one email into separate receipts. However, once the receipt is SmartScanned, users can [split the expense](https://help.expensify.com/articles/expensify-classic/expenses/Split-an-expense) in their Expensify account. + +**Can we set up a (co-marketing) partnership?** + +We currently do not offer any co-marketing partnerships. + +**Can we announce or advertise our custom integration with Expensify?** + +Absolutely! You can promote the integration across your social media channels (tag @expensify and use the #expensify hashtag) and you can even create your own dedicated landing page on your website for your integration. At a minimum, we recommend including a brief overview of how the integration works, the benefits of using it, an integration setup guide, and guidance for how someone can contact you for support or integration setup if necessary. + +**How can I get help?** + +You can contact Concierge for ongoing support any time by clicking the green chat icon in the mobile or web app, or by emailing concierge@expensify.com. Concierge is a global team of highly trained product specialists focused on making our product as easy to use as possible and answering all your questions. + +{% include faq-end.md %} From e141c3e675eef6bea861894fc77e385550d971ed Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Fri, 30 Aug 2024 12:24:41 -0500 Subject: [PATCH 002/273] Update redirects.csv Updated to include renaming of article --- docs/redirects.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/redirects.csv b/docs/redirects.csv index 480fd4220bd4..b06f017b6abd 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -569,3 +569,4 @@ https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2 https://community.expensify.com/discussion/5654/deep-dive-using-expense-rules-to-vendor-match-when-exporting-to-an-accounting-package/p1?new=1,https://help.expensify.com/articles/expensify-classic/connections/xero/Xero-Troubleshooting https://help.expensify.com/articles/expensify-classic/spending-insights/(https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates),https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-notifications,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-Notifications +https://help.expensify.com/articles/expensify-classic/connections/Additional-Travel-Integrations,https://help.expensify.com/articles/expensify-classic/connections/Travel-receipt-integrations From ddc6d7ac7bb0521110219b7d08041913e7b57e39 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 19 Sep 2024 00:48:33 +0530 Subject: [PATCH 003/273] adds v1 changes --- .../MoneyRequestConfirmationList.tsx | 2 + src/libs/OptionsListUtils.ts | 53 +++++++++++++++++++ src/libs/ReportUtils.ts | 2 + src/libs/actions/Report.ts | 6 +++ 4 files changed, 63 insertions(+) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 743a5b276c98..5f9f3f35e8b6 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -654,6 +654,8 @@ function MoneyRequestConfirmationList({ isSelected: false, isInteractive: !shouldDisableParticipant(participant), })); + + // console.log('formattedSelectedParticipants', formattedSelectedParticipants); options.push({ title: translate('common.to'), data: formattedSelectedParticipants, diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index afedd308371c..ac78c3c75d48 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -10,6 +10,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {SetNonNullable} from 'type-fest'; import {FallbackAvatar} from '@components/Icon/Expensicons'; import type {SelectedTagOption} from '@components/TagPicker'; +import {createDraftReportForPolicyExpenseChat} from '@libs/actions/Report'; import type {IOUAction} from '@src/CONST'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -61,6 +62,7 @@ import * as UserUtils from './UserUtils'; type SearchOption = ReportUtils.OptionData & { item: T; + isOptimisticReportOption?: boolean; }; type OptionList = { @@ -239,6 +241,13 @@ Onyx.connect({ }, }); +let allReportsDraft: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_DRAFT, + waitForCollectionCallback: true, + callback: (value) => (allReportsDraft = value), +}); + let loginList: OnyxEntry; Onyx.connect({ key: ONYXKEYS.LOGIN_LIST, @@ -1516,6 +1525,7 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: function createOptionList(personalDetails: OnyxEntry, reports?: OnyxCollection) { const reportMapForAccountIDs: Record = {}; const allReportOptions: Array> = []; + const policyToReportForPolicyExpenseChats: Record = {}; if (reports) { Object.values(reports).forEach((report) => { @@ -1531,6 +1541,10 @@ function createOptionList(personalDetails: OnyxEntry, repor return; } + if (ReportUtils.isPolicyExpenseChat(report) && report.policyID) { + policyToReportForPolicyExpenseChats[report.policyID] = report; + } + // Save the report in the map if this is a single participant so we can associate the reportID with the // personal detail option later. Individuals should not be associated with single participant // policyExpenseChats or chatRooms since those are not people. @@ -1545,6 +1559,45 @@ function createOptionList(personalDetails: OnyxEntry, repor }); } + const policiesWithoutExpenseChats = Object.values(policies ?? {}).filter((policy) => { + if (policy?.type === CONST.POLICY.TYPE.PERSONAL || !policy?.isPolicyExpenseChatEnabled) { + return false; + } + return !policyToReportForPolicyExpenseChats[policy?.id ?? '']; + }); + + // go through each policy and create a optimistic report option for it + if (policiesWithoutExpenseChats && policiesWithoutExpenseChats.length > 0) { + policiesWithoutExpenseChats.forEach((policy) => { + // check for draft report exist in allreportDrafts for the policy + let draftReport = Object.values(allReportsDraft ?? {})?.find((reportDraft) => reportDraft?.policyID === policy?.id); + if (!draftReport) { + draftReport = ReportUtils.buildOptimisticChatReport( + [currentUserAccountID ?? -1], + '', + CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + policy?.id, + currentUserAccountID, + true, + policy?.name, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + createDraftReportForPolicyExpenseChat({...draftReport, isOptimisticReport: true}); + } + const accountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(draftReport); + allReportOptions.push({ + item: draftReport, + ...createOption(accountIDs, personalDetails, draftReport, {}), + }); + }); + } const allPersonalDetailsOptions = Object.values(personalDetails ?? {}).map((personalDetail) => ({ item: personalDetail, ...createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], {}, {showPersonalDetails: true}), diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f37f3f940516..b408519e4c3a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -267,6 +267,7 @@ type OptimisticChatReport = Pick< | 'chatReportID' | 'iouReportID' | 'isOwnPolicyExpenseChat' + | 'isPolicyExpenseChat' | 'isPinned' | 'lastActorAccountID' | 'lastMessageTranslationKey' @@ -5099,6 +5100,7 @@ function buildOptimisticChatReport( chatType, isOwnPolicyExpenseChat, isPinned: isNewlyCreatedWorkspaceChat, + isPolicyExpenseChat: chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, lastActorAccountID: 0, lastMessageTranslationKey: '', lastMessageHtml: '', diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index caaf6840e56d..72b1a41646a6 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -76,6 +76,7 @@ import processReportIDDeeplink from '@libs/processReportIDDeeplink'; import * as Pusher from '@libs/Pusher/pusher'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportConnection from '@libs/ReportConnection'; +import type {OptimisticChatReport} from '@libs/ReportUtils'; import type {OptimisticAddCommentReportAction} from '@libs/ReportUtils'; import * as ReportUtils from '@libs/ReportUtils'; import {doesReportBelongToWorkspace} from '@libs/ReportUtils'; @@ -4097,6 +4098,10 @@ function markAsManuallyExported(reportID: string, connectionName: ConnectionName API.write(WRITE_COMMANDS.MARK_AS_EXPORTED, params, {optimisticData, successData, failureData}); } +function createDraftReportForPolicyExpenseChat(draftReport: OptimisticChatReport) { + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${draftReport.reportID}`, draftReport); +} + export type {Video}; export { @@ -4185,4 +4190,5 @@ export { exportToIntegration, markAsManuallyExported, handleReportChanged, + createDraftReportForPolicyExpenseChat, }; From db274ef4410788b033cc48587e2c37fc07d4749f Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 19 Sep 2024 01:15:38 +0530 Subject: [PATCH 004/273] lint fix --- src/libs/OptionsListUtils.ts | 2 +- src/libs/actions/Report.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index ac78c3c75d48..64b38e831d2c 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -10,7 +10,6 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {SetNonNullable} from 'type-fest'; import {FallbackAvatar} from '@components/Icon/Expensicons'; import type {SelectedTagOption} from '@components/TagPicker'; -import {createDraftReportForPolicyExpenseChat} from '@libs/actions/Report'; import type {IOUAction} from '@src/CONST'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -41,6 +40,7 @@ import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; +import {createDraftReportForPolicyExpenseChat} from './actions/Report'; import Timing from './actions/Timing'; import * as ErrorUtils from './ErrorUtils'; import filterArrayByMatch from './filterArrayByMatch'; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 72b1a41646a6..fb6cd7dc4168 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -76,8 +76,7 @@ import processReportIDDeeplink from '@libs/processReportIDDeeplink'; import * as Pusher from '@libs/Pusher/pusher'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportConnection from '@libs/ReportConnection'; -import type {OptimisticChatReport} from '@libs/ReportUtils'; -import type {OptimisticAddCommentReportAction} from '@libs/ReportUtils'; +import type {OptimisticAddCommentReportAction, OptimisticChatReport} from '@libs/ReportUtils'; import * as ReportUtils from '@libs/ReportUtils'; import {doesReportBelongToWorkspace} from '@libs/ReportUtils'; import shouldSkipDeepLinkNavigation from '@libs/shouldSkipDeepLinkNavigation'; From 2c3224b5d1a5cf6d9d0de6b4544edab193e73733 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 19 Sep 2024 01:21:06 +0530 Subject: [PATCH 005/273] remove undesired changes --- src/components/MoneyRequestConfirmationList.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 5f9f3f35e8b6..743a5b276c98 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -654,8 +654,6 @@ function MoneyRequestConfirmationList({ isSelected: false, isInteractive: !shouldDisableParticipant(participant), })); - - // console.log('formattedSelectedParticipants', formattedSelectedParticipants); options.push({ title: translate('common.to'), data: formattedSelectedParticipants, From eee62507fabbbd9a4a6b794e2b8a20508ec34570 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 19 Sep 2024 03:49:09 +0530 Subject: [PATCH 006/273] remove option from other lists --- src/libs/OptionsListUtils.ts | 8 ++++++++ .../iou/request/MoneyRequestParticipantsSelector.tsx | 1 + 2 files changed, 9 insertions(+) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 64b38e831d2c..45c659cf5c6d 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -181,6 +181,7 @@ type GetOptionsConfig = { includeDomainEmail?: boolean; action?: IOUAction; shouldBoldTitleByDefault?: boolean; + includePoliciesWithoutExpenseChats?: boolean; }; type GetUserToInviteConfig = { @@ -1594,6 +1595,7 @@ function createOptionList(personalDetails: OnyxEntry, repor const accountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(draftReport); allReportOptions.push({ item: draftReport, + isOptimisticReportOption: true, ...createOption(accountIDs, personalDetails, draftReport, {}), }); }); @@ -1797,6 +1799,7 @@ function getOptions( includeDomainEmail = false, action, shouldBoldTitleByDefault = true, + includePoliciesWithoutExpenseChats = false, }: GetOptionsConfig, ): Options { if (includeCategories) { @@ -1861,6 +1864,9 @@ function getOptions( // Filter out all the reports that shouldn't be displayed const filteredReportOptions = options.reports.filter((option) => { + if (option.isOptimisticReportOption && !includePoliciesWithoutExpenseChats) { + return; + } const report = option.item; const doesReportHaveViolations = shouldShowViolations(report, transactionViolations); @@ -2191,6 +2197,7 @@ function getFilteredOptions( includeInvoiceRooms = false, action: IOUAction | undefined = undefined, sortByReportTypeInSearch = false, + includePoliciesWithoutExpenseChats = false, ) { return getOptions( {reports, personalDetails}, @@ -2220,6 +2227,7 @@ function getFilteredOptions( includeInvoiceRooms, action, sortByReportTypeInSearch, + includePoliciesWithoutExpenseChats, }, ); } diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index f10575f8c1d0..247b413da319 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -130,6 +130,7 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF iouType === CONST.IOU.TYPE.INVOICE, action, isPaidGroupPolicy, + true, ); return optionList; From 14ffa198bab8d0d095a53eb04faab5ac0e3e0309 Mon Sep 17 00:00:00 2001 From: Getabalew Date: Thu, 19 Sep 2024 11:37:23 +0300 Subject: [PATCH 007/273] refactor: reuse ValidateCodeActionModal --- src/ROUTES.ts | 1 - src/SCREENS.ts | 1 - .../ValidateCodeForm/BaseValidateCodeForm.tsx | 15 +- .../ValidateCodeActionModal/index.tsx | 20 +- .../ValidateCodeActionModal/type.ts | 8 + .../ModalStackNavigators/index.tsx | 1 - .../CENTRAL_PANE_TO_RHP_MAPPING.ts | 1 - src/libs/Navigation/linkingConfig/config.ts | 3 - src/libs/actions/Delegate.ts | 9 +- .../Contacts/ContactMethodDetailsPage.tsx | 185 ++++++++-------- .../Profile/Contacts/ContactMethodsPage.tsx | 7 +- .../Profile/Contacts/NewContactMethodPage.tsx | 1 + .../Contacts/ValidateContactActionPage.tsx | 72 ------ .../AddDelegate/ConfirmDelegatePage.tsx | 6 +- .../AddDelegate/DelegateMagicCodePage.tsx | 58 +++-- .../ValidateCodeForm/BaseValidateCodeForm.tsx | 208 ------------------ .../ValidateCodeForm/index.android.tsx | 14 -- .../AddDelegate/ValidateCodeForm/index.tsx | 14 -- .../settings/Wallet/ExpensifyCardPage.tsx | 2 + 19 files changed, 163 insertions(+), 463 deletions(-) delete mode 100644 src/pages/settings/Profile/Contacts/ValidateContactActionPage.tsx delete mode 100644 src/pages/settings/Security/AddDelegate/ValidateCodeForm/BaseValidateCodeForm.tsx delete mode 100644 src/pages/settings/Security/AddDelegate/ValidateCodeForm/index.android.tsx delete mode 100644 src/pages/settings/Security/AddDelegate/ValidateCodeForm/index.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 27504998c49c..c9116f337f9e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -217,7 +217,6 @@ const ROUTES = { route: 'settings/profile/contact-methods/:contactMethod/details', getRoute: (contactMethod: string, backTo?: string) => getUrlWithBackToParam(`settings/profile/contact-methods/${encodeURIComponent(contactMethod)}/details`, backTo), }, - SETINGS_CONTACT_METHOD_VALIDATE_ACTION: 'settings/profile/contact-methods/validate-action', SETTINGS_NEW_CONTACT_METHOD: { route: 'settings/profile/contact-methods/new', getRoute: (backTo?: string) => getUrlWithBackToParam('settings/profile/contact-methods/new', backTo), diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 8168afba89ab..66cc2b420f44 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -74,7 +74,6 @@ const SCREENS = { DISPLAY_NAME: 'Settings_Display_Name', CONTACT_METHODS: 'Settings_ContactMethods', CONTACT_METHOD_DETAILS: 'Settings_ContactMethodDetails', - CONTACT_METHOD_VALIDATE_ACTION: 'Settings_ValidateContactMethodAction', NEW_CONTACT_METHOD: 'Settings_NewContactMethod', STATUS_CLEAR_AFTER: 'Settings_Status_Clear_After', STATUS_CLEAR_AFTER_DATE: 'Settings_Status_Clear_After_Date', diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx index 247c0c606901..f6df07278ad8 100644 --- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -64,6 +64,8 @@ type ValidateCodeFormProps = { /** Function to clear error of the form */ clearError: () => void; + + sendValidateCode: () => void; }; type BaseValidateCodeFormProps = BaseValidateCodeFormOnyxProps & ValidateCodeFormProps; @@ -78,6 +80,7 @@ function BaseValidateCodeForm({ validateError, handleSubmitForm, clearError, + sendValidateCode, }: BaseValidateCodeFormProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); @@ -128,14 +131,6 @@ function BaseValidateCodeForm({ }, []), ); - useEffect(() => { - if (!validateError) { - return; - } - clearError(); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [clearError, validateError]); - useEffect(() => { if (!hasMagicCodeBeenSent) { return; @@ -147,7 +142,7 @@ function BaseValidateCodeForm({ * Request a validate code / magic code be sent to verify this contact method */ const resendValidateCode = () => { - User.requestValidateCodeAction(); + sendValidateCode(); inputValidateCodeRef.current?.clear(); }; @@ -196,7 +191,7 @@ function BaseValidateCodeForm({ errorText={formError?.validateCode ? translate(formError?.validateCode) : ErrorUtils.getLatestErrorMessage(account ?? {})} hasError={!isEmptyObject(validateError)} onFulfill={validateAndSubmitForm} - autoFocus={false} + autoFocus /> (null); @@ -30,7 +42,8 @@ function ValidateCodeActionModal({isVisible, title, description, onClose, valida return; } firstRenderRef.current = false; - User.requestValidateCodeAction(); + + sendValidateCode(); }, [isVisible]); return ( @@ -61,10 +74,13 @@ function ValidateCodeActionModal({isVisible, title, description, onClose, valida validatePendingAction={validatePendingAction} validateError={validateError} handleSubmitForm={handleSubmitForm} + sendValidateCode={sendValidateCode} clearError={clearError} ref={validateCodeFormRef} + hasMagicCodeBeenSent={hasMagicCodeBeenSent} /> + {footer} ); diff --git a/src/components/ValidateCodeActionModal/type.ts b/src/components/ValidateCodeActionModal/type.ts index 3cbfe62513d1..821f54ff0302 100644 --- a/src/components/ValidateCodeActionModal/type.ts +++ b/src/components/ValidateCodeActionModal/type.ts @@ -1,3 +1,4 @@ +import React from 'react'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; type ValidateCodeActionModalProps = { @@ -24,6 +25,13 @@ type ValidateCodeActionModalProps = { /** Function to clear error of the form */ clearError: () => void; + + footer?: React.JSX.Element; + + sendValidateCode: () => void; + + /** If the magic code has been resent previously */ + hasMagicCodeBeenSent?: boolean; }; // eslint-disable-next-line import/prefer-default-export diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index b41b58530a6b..b24c6b3ea4f6 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -190,7 +190,6 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default, [SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: () => require('../../../../pages/settings/Profile/Contacts/ContactMethodsPage').default, [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS]: () => require('../../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default, - [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_VALIDATE_ACTION]: () => require('../../../../pages/settings/Profile/Contacts/ValidateContactActionPage').default, [SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: () => require('../../../../pages/settings/Profile/Contacts/NewContactMethodPage').default, [SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE]: () => require('../../../../pages/settings/Preferences/PriorityModePage').default, [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: () => require('../../../../pages/workspace/accounting/PolicyAccountingPage').default, 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 609162bedd13..3dc91a1bb530 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -6,7 +6,6 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = SCREENS.SETTINGS.PROFILE.DISPLAY_NAME, SCREENS.SETTINGS.PROFILE.CONTACT_METHODS, SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS, - SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_VALIDATE_ACTION, SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD, SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER, SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 2ca2db10a1a7..abfa9625926f 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -249,9 +249,6 @@ const config: LinkingOptions['config'] = { [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS]: { path: ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.route, }, - [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_VALIDATE_ACTION]: { - path: ROUTES.SETINGS_CONTACT_METHOD_VALIDATE_ACTION, - }, [SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: { path: ROUTES.SETTINGS_NEW_CONTACT_METHOD.route, exact: true, diff --git a/src/libs/actions/Delegate.ts b/src/libs/actions/Delegate.ts index 50d2ee7fc194..54165c4afa62 100644 --- a/src/libs/actions/Delegate.ts +++ b/src/libs/actions/Delegate.ts @@ -161,10 +161,6 @@ function clearDelegatorErrors() { Onyx.merge(ONYXKEYS.ACCOUNT, {delegatedAccess: {delegators: delegatedAccess.delegators.map((delegator) => ({...delegator, errorFields: undefined}))}}); } -function requestValidationCode() { - API.write(WRITE_COMMANDS.RESEND_VALIDATE_CODE, null); -} - function addDelegate(email: string, role: DelegateRole, validateCode: string) { const existingDelegate = delegatedAccess?.delegates?.find((delegate) => delegate.email === email); @@ -206,6 +202,7 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { delegatedAccess: { delegates: optimisticDelegateData(), }, + isLoading: true, }, }, ]; @@ -250,6 +247,7 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { delegatedAccess: { delegates: successDelegateData(), }, + isLoading: false, }, }, ]; @@ -292,6 +290,7 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { delegatedAccess: { delegates: failureDelegateData(), }, + isLoading: false, }, }, ]; @@ -325,4 +324,4 @@ function removePendingDelegate(email: string) { }); } -export {connect, disconnect, clearDelegatorErrors, addDelegate, requestValidationCode, clearAddDelegateErrors, removePendingDelegate}; +export {connect, disconnect, clearDelegatorErrors, addDelegate, clearAddDelegateErrors, removePendingDelegate}; diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx index 9fcc28f51912..e4751ebb0293 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx @@ -15,6 +15,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; +import ValidateCodeActionModal from '@components/ValidateCodeActionModal'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; @@ -25,6 +26,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; +import {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -35,12 +37,17 @@ import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeFo type ContactMethodDetailsPageProps = StackScreenProps; +type ValidateCodeFormError = { + validateCode?: TranslationPaths; +}; + function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { const [loginList, loginListResult] = useOnyx(ONYXKEYS.LOGIN_LIST); const [session, sessionResult] = useOnyx(ONYXKEYS.SESSION); const [myDomainSecurityGroups, myDomainSecurityGroupsResult] = useOnyx(ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS); const [securityGroups, securityGroupsResult] = useOnyx(ONYXKEYS.COLLECTION.SECURITY_GROUP); const [isLoadingReportData, isLoadingReportDataResult] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA, {initialValue: true}); + const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(true); const isLoadingOnyxValues = isLoadingOnyxValue(loginListResult, sessionResult, myDomainSecurityGroupsResult, securityGroupsResult, isLoadingReportDataResult); @@ -75,6 +82,7 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { }, [route.params.contactMethod]); const loginData = useMemo(() => loginList?.[contactMethod], [loginList, contactMethod]); const isDefaultContactMethod = useMemo(() => session?.email === loginData?.partnerUserID, [session?.email, loginData?.partnerUserID]); + const validateLoginError = ErrorUtils.getEarliestErrorField(loginData, 'validateLogin'); /** * Attempt to set this contact method as user's "Default contact method" @@ -145,6 +153,10 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo)); }, [prevValidatedDate, loginData?.validatedDate, isDefaultContactMethod, backTo]); + useEffect(() => { + setIsValidateCodeActionModalVisible(!loginData?.validatedDate && !loginData?.errorFields?.addedLogin); + }, [loginData?.validatedDate, loginData?.errorFields?.addedLogin]); + if (isLoadingOnyxValues || (isLoadingReportData && isEmptyObject(loginList))) { return ; } @@ -168,100 +180,97 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { const isFailedAddContactMethod = !!loginData.errorFields?.addedLogin; const isFailedRemovedContactMethod = !!loginData.errorFields?.deletedLogin; + const MenuItems = () => ( + <> + {canChangeDefaultContactMethod ? ( + User.clearContactMethodErrors(contactMethod, 'defaultLogin')} + > + + + ) : null} + {isDefaultContactMethod ? ( + User.clearContactMethodErrors(contactMethod, isFailedRemovedContactMethod ? 'deletedLogin' : 'defaultLogin')} + > + {translate('contacts.yourDefaultContactMethod')} + + ) : ( + User.clearContactMethodErrors(contactMethod, 'deletedLogin')} + > + toggleDeleteModal(true)} + /> + + )} + + ); + return ( - validateCodeFormRef.current?.focus?.()} - testID={ContactMethodDetailsPage.displayName} - > - Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo))} + + toggleDeleteModal(false)} + onModalHide={() => { + InteractionManager.runAfterInteractions(() => { + validateCodeFormRef.current?.focusLastSelected?.(); + }); + }} + prompt={translate('contacts.removeAreYouSure')} + confirmText={translate('common.yesContinue')} + cancelText={translate('common.cancel')} + isVisible={isDeleteModalOpen && !isDefaultContactMethod} + danger /> - - toggleDeleteModal(false)} - onModalHide={() => { - InteractionManager.runAfterInteractions(() => { - validateCodeFormRef.current?.focusLastSelected?.(); - }); + + {isFailedAddContactMethod && ( + { + User.clearContactMethod(contactMethod); + Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo)); }} - prompt={translate('contacts.removeAreYouSure')} - confirmText={translate('common.yesContinue')} - cancelText={translate('common.cancel')} - isVisible={isDeleteModalOpen && !isDefaultContactMethod} - danger + canDismissError /> + )} - {isFailedAddContactMethod && ( - { - User.clearContactMethod(contactMethod); - Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo)); - }} - canDismissError - /> - )} - - {!loginData.validatedDate && !isFailedAddContactMethod && ( - - + User.validateSecondaryLogin(loginList, contactMethod, validateCode)} + validateError={!isEmptyObject(validateLoginError) ? validateLoginError : ErrorUtils.getLatestErrorField(loginData, 'validateCodeSent')} + clearError={() => User.clearContactMethodErrors(contactMethod, !isEmptyObject(validateLoginError) ? 'validateLogin' : 'validateCodeSent')} + onClose={() => { + Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo)); + setIsValidateCodeActionModalVisible(false); + }} + sendValidateCode={() => User.requestContactMethodValidateCode(contactMethod)} + description={translate('contacts.enterMagicCode', {contactMethod})} + footer={} + /> - - - )} - {canChangeDefaultContactMethod ? ( - User.clearContactMethodErrors(contactMethod, 'defaultLogin')} - > - - - ) : null} - {isDefaultContactMethod ? ( - User.clearContactMethodErrors(contactMethod, isFailedRemovedContactMethod ? 'deletedLogin' : 'defaultLogin')} - > - {translate('contacts.yourDefaultContactMethod')} - - ) : ( - User.clearContactMethodErrors(contactMethod, 'deletedLogin')} - > - toggleDeleteModal(true)} - /> - - )} - - + {!isValidateCodeActionModalVisible && } + ); } diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx index 3f23b3a802be..cbe44ea648ca 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx @@ -81,12 +81,7 @@ function ContactMethodsPage({loginList, session, route}: ContactMethodsPageProps { - if (!login?.validatedDate && !login?.validateCodeSent) { - User.requestContactMethodValidateCode(loginName); - } - Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.getRoute(partnerUserID, navigateBackTo)); - }} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.getRoute(partnerUserID, navigateBackTo))} brickRoadIndicator={indicator} shouldShowBasicTitle shouldShowRightIcon diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index 4ea878e82987..6824b5988a62 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -153,6 +153,7 @@ function NewContactMethodPage({loginList, route}: NewContactMethodPageProps) { onClose={() => setIsValidateCodeActionModalVisible(false)} isVisible={isValidateCodeActionModalVisible} title={contactMethod} + sendValidateCode={() => User.requestValidateCodeAction()} description={translate('contacts.enterMagicCode', {contactMethod})} /> diff --git a/src/pages/settings/Profile/Contacts/ValidateContactActionPage.tsx b/src/pages/settings/Profile/Contacts/ValidateContactActionPage.tsx deleted file mode 100644 index 157588a67397..000000000000 --- a/src/pages/settings/Profile/Contacts/ValidateContactActionPage.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, {useEffect, useRef} from 'react'; -import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; -import DotIndicatorMessage from '@components/DotIndicatorMessage'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as User from '@libs/actions/User'; -import Navigation from '@libs/Navigation/Navigation'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import ValidateCodeForm from './ValidateCodeForm'; -import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeForm'; - -function ValidateContactActionPage() { - const [account] = useOnyx(ONYXKEYS.ACCOUNT); - const themeStyles = useThemeStyles(); - const {translate} = useLocalize(); - const validateCodeFormRef = useRef(null); - const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); - - const [pendingContactAction] = useOnyx(ONYXKEYS.PENDING_CONTACT_ACTION); - const loginData = loginList?.[pendingContactAction?.contactMethod ?? '']; - - useEffect(() => { - if (!loginData || !!loginData.pendingFields?.addedLogin) { - return; - } - - // Navigate to methods page on successful magic code verification - Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS.route); - }, [loginData, loginData?.pendingFields, loginList]); - - const onBackButtonPress = () => { - User.clearUnvalidatedNewContactMethodAction(); - Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route); - }; - - return ( - - - - - - - - ); -} - -ValidateContactActionPage.displayName = 'ValidateContactActionPage'; - -export default ValidateContactActionPage; diff --git a/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx index 8c8292b1f320..f54c43b73726 100644 --- a/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx +++ b/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx @@ -10,7 +10,6 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import {requestValidationCode} from '@libs/actions/Delegate'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -43,10 +42,7 @@ function ConfirmDelegatePage({route}: ConfirmDelegatePageProps) { text={translate('delegate.addCopilot')} style={styles.mt6} pressOnEnter - onPress={() => { - requestValidationCode(); - Navigation.navigate(ROUTES.SETTINGS_DELEGATE_MAGIC_CODE.getRoute(login, role)); - }} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_DELEGATE_MAGIC_CODE.getRoute(login, role))} /> ); diff --git a/src/pages/settings/Security/AddDelegate/DelegateMagicCodePage.tsx b/src/pages/settings/Security/AddDelegate/DelegateMagicCodePage.tsx index 9497507f041a..603fa1e5aa02 100644 --- a/src/pages/settings/Security/AddDelegate/DelegateMagicCodePage.tsx +++ b/src/pages/settings/Security/AddDelegate/DelegateMagicCodePage.tsx @@ -1,33 +1,31 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useEffect, useRef} from 'react'; +import React, {useEffect, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import Text from '@components/Text'; +import ValidateCodeActionModal from '@components/ValidateCodeActionModal'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; +import * as User from '@libs/actions/User'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import * as Delegate from '@userActions/Delegate'; import type CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import ValidateCodeForm from './ValidateCodeForm'; -import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeForm'; type DelegateMagicCodePageProps = StackScreenProps; function DelegateMagicCodePage({route}: DelegateMagicCodePageProps) { const {translate} = useLocalize(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(true); + const login = route.params.login; const role = route.params.role as ValueOf; - const styles = useThemeStyles(); - const validateCodeFormRef = useRef(null); - const currentDelegate = account?.delegatedAccess?.delegates?.find((d) => d.email === login); + const validateLoginError = ErrorUtils.getLatestErrorField(currentDelegate, 'addDelegate'); useEffect(() => { if (!currentDelegate || !!currentDelegate.pendingFields?.email || !!currentDelegate.errorFields?.addDelegate) { @@ -39,32 +37,28 @@ function DelegateMagicCodePage({route}: DelegateMagicCodePageProps) { }, [login, currentDelegate, role]); const onBackButtonPress = () => { + setIsValidateCodeActionModalVisible(false); Navigation.goBack(ROUTES.SETTINGS_DELEGATE_CONFIRM.getRoute(login, role)); }; + const clearError = () => { + if (!validateLoginError) { + return; + } + Delegate.clearAddDelegateErrors(currentDelegate?.email ?? '', 'addDelegate'); + }; + return ( - - {({safeAreaPaddingBottomStyle}) => ( - <> - - {translate('delegate.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})} - - - )} - + User.requestValidateCodeAction()} + handleSubmitForm={(validateCode) => Delegate.addDelegate(login, role, validateCode)} + description={translate('delegate.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})} + /> ); } diff --git a/src/pages/settings/Security/AddDelegate/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/settings/Security/AddDelegate/ValidateCodeForm/BaseValidateCodeForm.tsx deleted file mode 100644 index c9816862ad35..000000000000 --- a/src/pages/settings/Security/AddDelegate/ValidateCodeForm/BaseValidateCodeForm.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import {useFocusEffect} from '@react-navigation/native'; -import type {ForwardedRef} from 'react'; -import React, {useCallback, useImperativeHandle, useRef, useState} from 'react'; -import {View} from 'react-native'; -import type {StyleProp, ViewStyle} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; -import Button from '@components/Button'; -import FixedFooter from '@components/FixedFooter'; -import MagicCodeInput from '@components/MagicCodeInput'; -import type {AutoCompleteVariant, MagicCodeInputHandle} from '@components/MagicCodeInput'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import * as ValidationUtils from '@libs/ValidationUtils'; -import * as Delegate from '@userActions/Delegate'; -import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {DelegateRole} from '@src/types/onyx/Account'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; - -type ValidateCodeFormHandle = { - focus: () => void; - focusLastSelected: () => void; -}; - -type ValidateCodeFormError = { - validateCode?: TranslationPaths; -}; - -type BaseValidateCodeFormProps = { - /** Specifies autocomplete hints for the system, so it can provide autofill */ - autoComplete?: AutoCompleteVariant; - - /** Forwarded inner ref */ - innerRef?: ForwardedRef; - - /** The email of the delegate */ - delegate: string; - - /** The role of the delegate */ - role: DelegateRole; - - /** Any additional styles to apply */ - wrapperStyle?: StyleProp; -}; - -function BaseValidateCodeForm({autoComplete = 'one-time-code', innerRef = () => {}, delegate, role, wrapperStyle}: BaseValidateCodeFormProps) { - const {translate} = useLocalize(); - const {isOffline} = useNetwork(); - const theme = useTheme(); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const [formError, setFormError] = useState({}); - const [validateCode, setValidateCode] = useState(''); - const inputValidateCodeRef = useRef(null); - const [account] = useOnyx(ONYXKEYS.ACCOUNT); - const login = account?.primaryLogin; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case - const focusTimeoutRef = useRef(null); - - const currentDelegate = account?.delegatedAccess?.delegates?.find((d) => d.email === delegate); - const validateLoginError = ErrorUtils.getLatestErrorField(currentDelegate, 'addDelegate'); - - const shouldDisableResendValidateCode = !!isOffline || currentDelegate?.isLoading; - - useImperativeHandle(innerRef, () => ({ - focus() { - inputValidateCodeRef.current?.focus(); - }, - focusLastSelected() { - if (!inputValidateCodeRef.current) { - return; - } - if (focusTimeoutRef.current) { - clearTimeout(focusTimeoutRef.current); - } - focusTimeoutRef.current = setTimeout(() => { - inputValidateCodeRef.current?.focusLastSelected(); - }, CONST.ANIMATED_TRANSITION); - }, - })); - - useFocusEffect( - useCallback(() => { - if (!inputValidateCodeRef.current) { - return; - } - if (focusTimeoutRef.current) { - clearTimeout(focusTimeoutRef.current); - } - focusTimeoutRef.current = setTimeout(() => { - inputValidateCodeRef.current?.focusLastSelected(); - }, CONST.ANIMATED_TRANSITION); - return () => { - if (!focusTimeoutRef.current) { - return; - } - clearTimeout(focusTimeoutRef.current); - }; - }, []), - ); - - /** - * Request a validate code / magic code be sent to verify this contact method - */ - const resendValidateCode = () => { - if (!login) { - return; - } - Delegate.requestValidationCode(); - - inputValidateCodeRef.current?.clear(); - }; - - /** - * Handle text input and clear formError upon text change - */ - const onTextInput = useCallback( - (text: string) => { - setValidateCode(text); - setFormError({}); - if (validateLoginError) { - Delegate.clearAddDelegateErrors(currentDelegate?.email ?? '', 'addDelegate'); - } - }, - [currentDelegate?.email, validateLoginError], - ); - - /** - * Check that all the form fields are valid, then trigger the submit callback - */ - const validateAndSubmitForm = useCallback(() => { - if (!validateCode.trim()) { - setFormError({validateCode: 'validateCodeForm.error.pleaseFillMagicCode'}); - return; - } - - if (!ValidationUtils.isValidValidateCode(validateCode)) { - setFormError({validateCode: 'validateCodeForm.error.incorrectMagicCode'}); - return; - } - - setFormError({}); - - Delegate.addDelegate(delegate, role, validateCode); - }, [delegate, role, validateCode]); - - return ( - - - - - - - {translate('validateCodeForm.magicCodeNotReceived')} - - - - - - -