diff --git a/src/CONST.ts b/src/CONST.ts index cd43b358011a..d0695b1e285f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4,6 +4,7 @@ import dateSubtract from 'date-fns/sub'; import Config from 'react-native-config'; import * as KeyCommand from 'react-native-key-command'; import type {ValueOf} from 'type-fest'; +import type {Video} from './libs/actions/Report'; import BankAccount from './libs/models/BankAccount'; import * as Url from './libs/Url'; import SCREENS from './SCREENS'; @@ -64,16 +65,91 @@ const chatTypes = { // Explicit type annotation is required const cardActiveStates: number[] = [2, 3, 4, 7]; -const onboardingChoices = { +const selectableOnboardingChoices = { PERSONAL_SPEND: 'newDotPersonalSpend', MANAGE_TEAM: 'newDotManageTeam', EMPLOYER: 'newDotEmployer', CHAT_SPLIT: 'newDotSplitChat', LOOKING_AROUND: 'newDotLookingAround', +} as const; + +const backendOnboardingChoices = { + SUBMIT: 'newDotSubmit', +} as const; + +const onboardingChoices = { + ...selectableOnboardingChoices, + ...backendOnboardingChoices, +} as const; + +const onboardingEmployerOrSubmitMessage: OnboardingMessageType = { + message: 'Getting paid back is as easy as sending a message. Let’s go over the basics.', + video: { + url: `${CLOUDFRONT_URL}/videos/guided-setup-get-paid-back-v2.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-get-paid-back.jpg`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + type: 'submitExpense', + autoCompleted: false, + title: 'Submit an expense', + description: + '*Submit an expense* by entering an amount or scanning a receipt.\n' + + '\n' + + 'Here’s how to submit an expense:\n' + + '\n' + + '1. Click the green *+* button.\n' + + '2. Choose *Submit expense*.\n' + + '3. Enter an amount or scan a receipt.\n' + + '4. Add your reimburser to the request.\n' + + '\n' + + 'Then, send your request and wait for that sweet “Cha-ching!” when it’s complete.', + }, + { + type: 'enableWallet', + autoCompleted: false, + title: 'Enable your wallet', + description: + 'You’ll need to *enable your Expensify Wallet* to get paid back. Don’t worry, it’s easy!\n' + + '\n' + + 'Here’s how to set up your wallet:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Click *Wallet* > *Enable wallet*.\n' + + '3. Connect your bank account.\n' + + '\n' + + 'Once that’s done, you can request money from anyone and get paid back right into your personal bank account.', + }, + ], }; type OnboardingPurposeType = ValueOf; +const onboardingInviteTypes = { + IOU: 'iou', + INVOICE: 'invoice', + CHAT: 'chat', +} as const; + +type OnboardingInviteType = ValueOf; + +type OnboardingTaskType = { + type: string; + autoCompleted: boolean; + title: string; + description: string | ((params: Partial<{adminsRoomLink: string; workspaceCategoriesLink: string; workspaceMoreFeaturesLink: string; workspaceMembersLink: string}>) => string); +}; + +type OnboardingMessageType = { + message: string; + video?: Video; + tasks: OnboardingTaskType[]; + type?: string; +}; + const CONST = { HEIC_SIGNATURES: [ '6674797068656963', // 'ftypheic' - Indicates standard HEIC file @@ -1920,6 +1996,11 @@ const CONST = { BUSINESS_BANK_ACCOUNT: 'businessBankAccount', }, + PAYMENT_SELECTED: { + BBA: 'BBA', + PBA: 'PBA', + }, + PAYMENT_METHOD_ID_KEYS: { DEBIT_CARD: 'fundID', BANK_ACCOUNT: 'bankAccountID', @@ -1994,6 +2075,10 @@ const CONST = { ACCESS_VARIANTS: { CREATE: 'create', }, + PAYMENT_SELECTED: { + BBA: 'BBA', + PBA: 'PBA', + }, }, GROWL: { @@ -4296,6 +4381,8 @@ const CONST = { ONBOARDING_INTRODUCTION: 'Let’s get you set up 🔧', ONBOARDING_CHOICES: {...onboardingChoices}, + SELECTABLE_ONBOARDING_CHOICES: {...selectableOnboardingChoices}, + ONBOARDING_INVITE_TYPES: {...onboardingInviteTypes}, ACTIONABLE_TRACK_EXPENSE_WHISPER_MESSAGE: 'What would you like to do with this expense?', ONBOARDING_CONCIERGE: { [onboardingChoices.EMPLOYER]: @@ -4338,49 +4425,8 @@ const CONST = { }, ONBOARDING_MESSAGES: { - [onboardingChoices.EMPLOYER]: { - message: 'Getting paid back is as easy as sending a message. Let’s go over the basics.', - video: { - url: `${CLOUDFRONT_URL}/videos/guided-setup-get-paid-back-v2.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-get-paid-back.jpg`, - duration: 55, - width: 1280, - height: 960, - }, - tasks: [ - { - type: 'submitExpense', - autoCompleted: false, - title: 'Submit an expense', - description: - '*Submit an expense* by entering an amount or scanning a receipt.\n' + - '\n' + - 'Here’s how to submit an expense:\n' + - '\n' + - '1. Click the green *+* button.\n' + - '2. Choose *Submit expense*.\n' + - '3. Enter an amount or scan a receipt.\n' + - '4. Add your reimburser to the request.\n' + - '\n' + - 'Then, send your request and wait for that sweet “Cha-ching!” when it’s complete.', - }, - { - type: 'enableWallet', - autoCompleted: false, - title: 'Enable your wallet', - description: - 'You’ll need to *enable your Expensify Wallet* to get paid back. Don’t worry, it’s easy!\n' + - '\n' + - 'Here’s how to set up your wallet:\n' + - '\n' + - '1. Click your profile picture.\n' + - '2. Click *Wallet* > *Enable wallet*.\n' + - '3. Connect your bank account.\n' + - '\n' + - 'Once that’s done, you can request money from anyone and get paid back right into your personal bank account.', - }, - ], - }, + [onboardingChoices.EMPLOYER]: onboardingEmployerOrSubmitMessage, + [onboardingChoices.SUBMIT]: onboardingEmployerOrSubmitMessage, [onboardingChoices.MANAGE_TEAM]: { message: 'Here are some important tasks to help get your team’s expenses under control.', video: { @@ -4409,7 +4455,7 @@ const CONST = { type: 'meetGuide', autoCompleted: false, title: 'Meet your setup specialist', - description: ({adminsRoomLink}: {adminsRoomLink: string}) => + description: ({adminsRoomLink}) => `Meet your setup specialist, who can answer any questions as you get started with Expensify. Yes, a real human!\n` + '\n' + `Chat with the specialist in your [#admins room](${adminsRoomLink}).`, @@ -4418,7 +4464,7 @@ const CONST = { type: 'setupCategories', autoCompleted: false, title: 'Set up categories', - description: ({workspaceCategoriesLink}: {workspaceCategoriesLink: string}) => + description: ({workspaceCategoriesLink}) => '*Set up categories* so your team can code expenses for easy reporting.\n' + '\n' + 'Here’s how to set up categories:\n' + @@ -4437,7 +4483,7 @@ const CONST = { type: 'addExpenseApprovals', autoCompleted: false, title: 'Add expense approvals', - description: ({workspaceMoreFeaturesLink}: {workspaceMoreFeaturesLink: string}) => + description: ({workspaceMoreFeaturesLink}) => '*Add expense approvals* to review your team’s spend and keep it under control.\n' + '\n' + 'Here’s how to add expense approvals:\n' + @@ -4456,7 +4502,7 @@ const CONST = { type: 'inviteTeam', autoCompleted: false, title: 'Invite your team', - description: ({workspaceMembersLink}: {workspaceMembersLink: string}) => + description: ({workspaceMembersLink}) => '*Invite your team* to Expensify so they can start tracking expenses today.\n' + '\n' + 'Here’s how to invite your team:\n' + @@ -4565,7 +4611,7 @@ const CONST = { "Expensify is best known for expense and corporate card management, but we do a lot more than that. Let me know what you're interested in and I'll help get you started.", tasks: [], }, - }, + } satisfies Record, REPORT_FIELD_TITLE_FIELD_ID: 'text_title', @@ -5648,6 +5694,6 @@ type FeedbackSurveyOptionID = ValueOf; type CancellationType = ValueOf; -export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SubscriptionType, FeedbackSurveyOptionID, CancellationType}; +export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SubscriptionType, FeedbackSurveyOptionID, CancellationType, OnboardingInviteType}; export default CONST; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d7749ed20158..b291eda984da 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,5 +1,6 @@ import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; +import type {OnboardingPurposeType} from './CONST'; import type * as FormTypes from './types/form'; import type * as OnyxTypes from './types/onyx'; import type Onboarding from './types/onyx/Onboarding'; @@ -905,7 +906,7 @@ type OnyxValuesMapping = { [ONYXKEYS.MAX_CANVAS_AREA]: number; [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; - [ONYXKEYS.ONBOARDING_PURPOSE_SELECTED]: string; + [ONYXKEYS.ONBOARDING_PURPOSE_SELECTED]: OnboardingPurposeType; [ONYXKEYS.ONBOARDING_ERROR_MESSAGE]: string; [ONYXKEYS.ONBOARDING_POLICY_ID]: string; [ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID]: string; diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 2ccdd47c3205..1cd1bfb36d83 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -12,7 +12,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetails, PersonalDetailsList, Policy, Report, ReportActions} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, Report, ReportActions} from '@src/types/onyx'; import CaretWrapper from './CaretWrapper'; import DisplayNames from './DisplayNames'; import MultipleAvatars from './MultipleAvatars'; @@ -69,7 +69,7 @@ function AvatarWithDisplayName({ ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report) || ReportUtils.isInvoiceReport(report); const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy, invoiceReceiverPolicy); const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails) as PersonalDetails[], false); + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails), false); const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(report); const avatarBorderColor = isAnonymous ? theme.highlightBG : theme.componentBG; diff --git a/src/libs/API/parameters/CompleteGuidedSetupParams.ts b/src/libs/API/parameters/CompleteGuidedSetupParams.ts index 8e1273ac6053..0b2c0b66ef0a 100644 --- a/src/libs/API/parameters/CompleteGuidedSetupParams.ts +++ b/src/libs/API/parameters/CompleteGuidedSetupParams.ts @@ -6,6 +6,7 @@ type CompleteGuidedSetupParams = { actorAccountID: number; guidedSetupData: string; engagementChoice: OnboardingPurposeType; + paymentSelected?: string; }; export default CompleteGuidedSetupParams; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 83fad079f649..d5b95e8e0485 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -7,6 +7,7 @@ import lodashSet from 'lodash/set'; import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; 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 type {IOUAction} from '@src/CONST'; @@ -380,8 +381,8 @@ function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: OnyxEntr * Returns the personal details for an array of accountIDs * @returns keys of the object are emails, values are PersonalDetails objects. */ -function getPersonalDetailsForAccountIDs(accountIDs: number[] | undefined, personalDetails: OnyxInputOrEntry): PersonalDetailsList { - const personalDetailsForAccountIDs: PersonalDetailsList = {}; +function getPersonalDetailsForAccountIDs(accountIDs: number[] | undefined, personalDetails: OnyxInputOrEntry): SetNonNullable { + const personalDetailsForAccountIDs: SetNonNullable = {}; if (!personalDetails) { return personalDetailsForAccountIDs; } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 735011e455cd..1e5965be8d5e 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -296,7 +296,7 @@ function getOptionData({ const participantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report); const visibleParticipantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report, true); - const participantPersonalDetailList = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails)) as PersonalDetails[]; + const participantPersonalDetailList = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails)); const personalDetail = participantPersonalDetailList[0] ?? {}; const hasErrors = Object.keys(result.allReportErrors ?? {}).length !== 0; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 874583868381..f3c406a8f04e 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -41,12 +41,14 @@ import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralP import Navigation from '@libs/Navigation/Navigation'; import * as NextStepUtils from '@libs/NextStepUtils'; import {rand64} from '@libs/NumberUtils'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportConnection from '@libs/ReportConnection'; import type {OptimisticChatReport, OptimisticCreatedReportAction, OptimisticIOUReportAction, TransactionDetails} from '@libs/ReportUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import * as SessionUtils from '@libs/SessionUtils'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import {getCurrency, getTransaction} from '@libs/TransactionUtils'; @@ -272,6 +274,18 @@ Onyx.connect({ callback: (value) => (activePolicyID = value), }); +let introSelected: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.NVP_INTRO_SELECTED, + callback: (value) => (introSelected = value), +}); + +let personalDetailsList: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + callback: (value) => (personalDetailsList = value), +}); + /** * Find the report preview action from given chat report and iou report */ @@ -7391,12 +7405,52 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O ); } +/** + * Completes onboarding for invite link flow based on the selected payment option + * + * @param paymentSelected based on which we choose the onboarding choice and concierge message + */ +function completePaymentOnboarding(paymentSelected: ValueOf) { + const isInviteOnboardingComplete = introSelected?.isInviteOnboardingComplete ?? false; + + if (isInviteOnboardingComplete || !introSelected?.choice) { + return; + } + + const session = SessionUtils.getSession(); + + const personalDetailsListValues = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(session?.accountID ? [session.accountID] : [], personalDetailsList)); + const personalDetails = personalDetailsListValues[0] ?? {}; + + let onboardingPurpose = introSelected.choice; + if (introSelected.inviteType === CONST.ONBOARDING_INVITE_TYPES.IOU && paymentSelected === CONST.IOU.PAYMENT_SELECTED.BBA) { + onboardingPurpose = CONST.ONBOARDING_CHOICES.MANAGE_TEAM; + } + + if (introSelected.inviteType === CONST.ONBOARDING_INVITE_TYPES.INVOICE && paymentSelected !== CONST.IOU.PAYMENT_SELECTED.BBA) { + onboardingPurpose = CONST.ONBOARDING_CHOICES.CHAT_SPLIT; + } + + Report.completeOnboarding( + onboardingPurpose, + CONST.ONBOARDING_MESSAGES[onboardingPurpose], + { + firstName: personalDetails.firstName ?? '', + lastName: personalDetails.lastName ?? '', + }, + paymentSelected, + ); +} + function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.Report, iouReport: OnyxTypes.Report, full = true) { if (chatReport.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(chatReport.policyID)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(chatReport.policyID)); return; } + const paymentSelected = paymentType === CONST.IOU.PAYMENT_TYPE.VBBA ? CONST.IOU.PAYMENT_SELECTED.BBA : CONST.IOU.PAYMENT_SELECTED.PBA; + completePaymentOnboarding(paymentSelected); + const recipient = {accountID: iouReport.ownerAccountID}; const {params, optimisticData, successData, failureData} = getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentType, full); @@ -7416,6 +7470,9 @@ function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes. params: {reportActionID}, } = getPayMoneyRequestParams(chatReport, invoiceReport, recipient, paymentMethodType, true, payAsBusiness); + const paymentSelected = paymentMethodType === CONST.IOU.PAYMENT_TYPE.VBBA ? CONST.IOU.PAYMENT_SELECTED.BBA : CONST.IOU.PAYMENT_SELECTED.PBA; + completePaymentOnboarding(paymentSelected); + const params: PayInvoiceParams = { reportID: invoiceReport.reportID, reportActionID, @@ -8011,6 +8068,7 @@ export { getIOURequestPolicyID, initMoneyRequest, navigateToStartStepIfScanFileCannotBeRead, + completePaymentOnboarding, payInvoice, payMoneyRequest, putOnHold, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 0c0aa02f6111..e9703d48d947 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3319,6 +3319,7 @@ function completeOnboarding( }, adminsChatReportID?: string, onboardingPolicyID?: string, + paymentSelected?: string, ) { const actorAccountID = CONST.ACCOUNT_ID.CONCIERGE; const targetChatReport = ReportUtils.getChatByParticipants([actorAccountID, currentUserAccountID]); @@ -3344,7 +3345,7 @@ function completeOnboarding( let videoCommentAction: OptimisticAddCommentReportAction | null = null; let videoMessage: AddCommentOrAttachementParams | null = null; - if (data.video) { + if ('video' in data && data.video) { const videoComment = ReportUtils.buildOptimisticAddCommentReportAction(CONST.ATTACHMENT_MESSAGE_TEXT, undefined, actorAccountID, 2); videoCommentAction = videoComment.reportAction; videoMessage = { @@ -3626,7 +3627,7 @@ function completeOnboarding( {type: 'message', ...textMessage}, ]; - if (data.video && videoCommentAction && videoMessage) { + if ('video' in data && data.video && videoCommentAction && videoMessage) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`, @@ -3664,6 +3665,7 @@ function completeOnboarding( lastName, actorAccountID, guidedSetupData: JSON.stringify(guidedSetupData), + paymentSelected, }; API.write(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP, parameters, {optimisticData, successData, failureData}); @@ -4074,6 +4076,8 @@ function markAsManuallyExported(reportID: string, connectionName: ConnectionName API.write(WRITE_COMMANDS.MARK_AS_EXPORTED, params, {optimisticData, successData, failureData}); } +export type {Video}; + export { searchInServer, addComment, diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx index 71ace34011b1..dc33d82b2a04 100644 --- a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx +++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx @@ -49,7 +49,7 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, ro const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; const paddingHorizontal = onboardingIsMediumOrLargerScreenWidth ? styles.ph8 : styles.ph5; - const menuItems: MenuItemProps[] = Object.values(CONST.ONBOARDING_CHOICES).map((choice) => { + const menuItems: MenuItemProps[] = Object.values(CONST.SELECTABLE_ONBOARDING_CHOICES).map((choice) => { const translationKey = `onboarding.purpose.${choice}` as const; return { key: translationKey, diff --git a/src/pages/OnboardingWork/BaseOnboardingWork.tsx b/src/pages/OnboardingWork/BaseOnboardingWork.tsx index db624bb9928e..f803b4e34a65 100644 --- a/src/pages/OnboardingWork/BaseOnboardingWork.tsx +++ b/src/pages/OnboardingWork/BaseOnboardingWork.tsx @@ -25,8 +25,6 @@ import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/WorkForm'; import type {BaseOnboardingWorkOnyxProps, BaseOnboardingWorkProps} from './types'; -const OPEN_WORK_PAGE_PURPOSES = [CONST.ONBOARDING_CHOICES.MANAGE_TEAM]; - function BaseOnboardingWork({shouldUseNativeStyles, onboardingPurposeSelected, onboardingPolicyID, route}: BaseOnboardingWorkProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -80,7 +78,7 @@ function BaseOnboardingWork({shouldUseNativeStyles, onboardingPurposeSelected, o ; export default IntroSelected;