diff --git a/.github/workflows/androidBump.yml b/.github/workflows/androidBump.yml index 8a4f7d514208..b2276551852f 100644 --- a/.github/workflows/androidBump.yml +++ b/.github/workflows/androidBump.yml @@ -1,6 +1,7 @@ name: Android Rollout Bumper on: + workflow_dispatch: schedule: # Runs at midnight every day - cron: '0 0 * * *' diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 1bba3e96735a..035879bf9727 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -60,6 +60,24 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + postGitHubCommentBuildStarted: + runs-on: ubuntu-latest + needs: [validateActor, getBranchRef] + if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} + steps: + - name: Add build start comment + uses: actions/github-script@v7 + with: + github-token: ${{ github.token }} + script: | + const workflowURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: process.env.PULL_REQUEST_NUMBER, + body: `馃毀 @${{ github.actor }} has triggered a test build. You can view the [workflow run here](${workflowURL}).` + }); + buildAndroid: name: Build Android app for testing uses: ./.github/workflows/buildAndroid.yml diff --git a/android/app/build.gradle b/android/app/build.gradle index e1e83a4b1b74..5fd9f28f0732 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009006204 - versionName "9.0.62-4" + versionCode 1009006303 + versionName "9.0.63-3" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/simple-illustrations/simple-illustration__pillow.svg b/assets/images/simple-illustrations/simple-illustration__pillow.svg new file mode 100644 index 000000000000..97a0811266ae --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__pillow.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md b/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md index a6e19f8fd549..3fd1df0c0a1c 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md +++ b/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md @@ -40,7 +40,6 @@ The following steps help you determine how data will be exported from Expensify - Journal Entries - This is a single itemized journal entry for each Expensify report. - _Non-reimbursable expenses_: Non-reimbursable expenses export to QuickBooks Online as: - Credit Card expenses - Each expense will be exported as a bank transaction with its transaction date. - - Note: The Expensify Card transactions will always export as Credit Card charges, even if the non-reimbursable setting is configured differently (such as a Vendor Bill.) - Debit Card Expenses - Each expense will be exported as a bank transaction with its transaction date. - Vendor Bills - A single detailed vendor bill is generated for each Expensify report. - If the accounting period is closed, the vendor bill will be posted on the first day of the next open period. If you choose to export non-reimbursable expenses as Vendor Bills, you can assign a default vendor to the bill. diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 1c8b62cfb579..dcf7c9f238a6 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -265,11 +265,15 @@ platform :android do desc "Submit HybridApp to 100% rollout on Google Play" lane :complete_hybrid_rollout do - productionVersionCode = google_play_track_version_codes(track: 'production') + productionVersionCodes = google_play_track_version_codes( + track: 'production', + package_name: "org.me.mobiexpensifyg", + json_key: './android/app/android-fastlane-json-key.json', + ) upload_to_play_store( package_name: "org.me.mobiexpensifyg", json_key: './android/app/android-fastlane-json-key.json', - version_code: productionVersionCode, + version_code: productionVersionCodes.sort.last, # Get the latest version code track: 'production', rollout: '1', skip_upload_apk: true, @@ -283,11 +287,15 @@ platform :android do desc "Update HybridApp rollout percentage on Google Play" lane :update_hybrid_rollout do |options| - productionVersionCode = google_play_track_version_codes(track: 'production') + productionVersionCodes = google_play_track_version_codes( + track: 'production', + package_name: "org.me.mobiexpensifyg", + json_key: './android/app/android-fastlane-json-key.json', + ) upload_to_play_store( package_name: "org.me.mobiexpensifyg", json_key: './android/app/android-fastlane-json-key.json', - version_code: productionVersionCode, + version_code: productionVersionCodes.sort.last, # Get the latest version code track: 'production', rollout: options[:rollout], skip_upload_apk: true, diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index dfaf2aa63578..d2577911d8e8 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.62 + 9.0.63 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.62.4 + 9.0.63.3 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 17afe7d5c774..3b6df8fa0017 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.62 + 9.0.63 CFBundleSignature ???? CFBundleVersion - 9.0.62.4 + 9.0.63.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 1fd4a6e1567a..47515260d65c 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.62 + 9.0.63 CFBundleVersion - 9.0.62.4 + 9.0.63.3 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 7cfa7214dcba..4caba424161e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.62-4", + "version": "9.0.63-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.62-4", + "version": "9.0.63-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 6d10be51df3c..766126a91393 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.62-4", + "version": "9.0.63-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/patches/react-native-reanimated+3.16.1+003+include-missing-header.patch b/patches/react-native-reanimated+3.16.1+003+include-missing-header.patch new file mode 100644 index 000000000000..80244991a890 --- /dev/null +++ b/patches/react-native-reanimated+3.16.1+003+include-missing-header.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp b/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp +index 475ec7a..832fb06 100644 +--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp ++++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp +@@ -32,6 +32,8 @@ + + #ifdef RCT_NEW_ARCH_ENABLED + #include ++#include ++#include + #endif // RCT_NEW_ARCH_ENABLED + + // Standard `__cplusplus` macro reference: \ No newline at end of file diff --git a/src/CONST.ts b/src/CONST.ts index 7c8a6791d65b..051fe789751d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -274,6 +274,7 @@ type OnboardingTask = { workspaceMembersLink: string; integrationName: string; workspaceAccountingLink: string; + workspaceSettingsLink: string; navatticURL: string; }>, ) => string); @@ -621,6 +622,14 @@ const CONST = { }, STEP_NAMES: ['1', '2', '3', '4', '5', '6'], STEP_HEADER_HEIGHT: 40, + SIGNER_INFO_STEP: { + SUBSTEP: { + IS_DIRECTOR: 1, + ENTER_EMAIL: 2, + SIGNER_DETAILS_FORM: 3, + HANG_TIGHT: 4, + }, + }, }, INCORPORATION_TYPES: { LLC: 'LLC', @@ -4903,6 +4912,15 @@ const CONST = { '\n' + `Chat with the specialist in your [#admins room](${adminsRoomLink}).`, }, + { + type: 'setupCategoriesAndTags', + autoCompleted: false, + title: 'Set up categories and tags', + description: ({workspaceSettingsLink, workspaceAccountingLink}) => + '*Set up categories and tags* so your team can code expenses for easy reporting.\n' + + '\n' + + `Import them automatically by [connecting your accounting software](${workspaceAccountingLink}), or set them up manually in your [workspace settings](${workspaceSettingsLink}).`, + }, { type: 'setupCategories', autoCompleted: false, diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 6b625f312709..0068fd30ed60 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -114,6 +114,7 @@ import PalmTree from '@assets/images/simple-illustrations/simple-illustration__p import Pencil from '@assets/images/simple-illustrations/simple-illustration__pencil.svg'; import PerDiem from '@assets/images/simple-illustrations/simple-illustration__perdiem.svg'; import PiggyBank from '@assets/images/simple-illustrations/simple-illustration__piggybank.svg'; +import Pillow from '@assets/images/simple-illustrations/simple-illustration__pillow.svg'; import Profile from '@assets/images/simple-illustrations/simple-illustration__profile.svg'; import QRCode from '@assets/images/simple-illustrations/simple-illustration__qr-code.svg'; import ReceiptEnvelope from '@assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg'; @@ -234,6 +235,7 @@ export { ExpensifyCardIllustration, SplitBill, PiggyBank, + Pillow, Accounting, Car, Coins, diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 2dc809f9ce68..d01b69ed5649 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -1,3 +1,4 @@ +import {useRoute} from '@react-navigation/native'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -9,8 +10,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {getCurrentUserAccountID} from '@libs/actions/Report'; import * as CurrencyUtils from '@libs/CurrencyUtils'; -import isReportOpenInRHP from '@libs/Navigation/isReportOpenInRHP'; -import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; +import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -23,6 +23,7 @@ import useDelegateUserDetails from '@src/hooks/useDelegateUserDetails'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -65,6 +66,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use a correct layout for the hold expense modal https://github.com/Expensify/App/pull/47990#issuecomment-2362382026 // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); + const route = useRoute(); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID ?? '-1'}`); const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID ?? '-1'}`); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`); @@ -175,7 +177,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails(); const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); - const isReportInRHP = isReportOpenInRHP(navigationRef?.getRootState()); + const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP; const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth; const confirmPayment = useCallback( diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 93ac363cff62..f253c757050f 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -1,3 +1,4 @@ +import {useRoute} from '@react-navigation/native'; import type {ReactNode} from 'react'; import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; @@ -7,8 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import isReportOpenInRHP from '@libs/Navigation/isReportOpenInRHP'; -import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; +import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -18,6 +18,7 @@ import * as IOU from '@userActions/IOU'; import * as TransactionActions from '@userActions/Transaction'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import type {Policy, Report, ReportAction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; @@ -48,6 +49,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use a correct layout for the hold expense modal https://github.com/Expensify/App/pull/47990#issuecomment-2362382026 // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); + const route = useRoute(); const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? '-1'}`); const [transaction] = useOnyx( `${ONYXKEYS.COLLECTION.TRANSACTION}${ @@ -65,7 +67,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID ?? ''); const reportID = report?.reportID; - const isReportInRHP = isReportOpenInRHP(navigationRef?.getRootState()); + const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP; const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth; const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID ?? '-1']); diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 3d6ad9006dc5..f1a72cc7fb8e 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -4,6 +4,7 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import Navigation from '@libs/Navigation/Navigation'; import {isLinkedTransactionHeld} from '@libs/ReportActionsUtils'; +import playSound, {SOUNDS} from '@libs/Sound'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -74,6 +75,7 @@ function ProcessMoneyReportHoldMenu({ if (startAnimation) { startAnimation(); } + playSound(SOUNDS.SUCCESS); IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport, full); } onClose(); diff --git a/src/components/RadioButton.tsx b/src/components/RadioButton.tsx index 0bf7e370e480..1ee885681700 100644 --- a/src/components/RadioButton.tsx +++ b/src/components/RadioButton.tsx @@ -41,8 +41,8 @@ function RadioButton({isChecked, onPress, accessibilityLabel, hasError = false, )} diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index c2b2d462aa41..ed1d8fd73565 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -213,7 +213,9 @@ function SettlementButton({ return; } - playSound(SOUNDS.SUCCESS); + if (!ReportUtils.hasHeldExpenses(iouReport?.reportID)) { + playSound(SOUNDS.SUCCESS); + } onPress(iouPaymentType); }; diff --git a/src/components/SubStepForms/DateOfBirthStep.tsx b/src/components/SubStepForms/DateOfBirthStep.tsx index 42077cef2ba1..934a50581f30 100644 --- a/src/components/SubStepForms/DateOfBirthStep.tsx +++ b/src/components/SubStepForms/DateOfBirthStep.tsx @@ -9,7 +9,6 @@ import useLocalize from '@hooks/useLocalize'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ValidationUtils from '@libs/ValidationUtils'; -import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; import type {OnyxFormValuesMapping} from '@src/ONYXKEYS'; @@ -35,8 +34,8 @@ type DateOfBirthStepProps = SubStep /** The default value for the date of birth input */ dobDefaultValue: string; - /** Whether the component should show help links */ - shouldShowHelpLinks?: boolean; + /** Optional footer component */ + footerComponent?: React.ReactNode; }; function DateOfBirthStep({ @@ -48,7 +47,7 @@ function DateOfBirthStep({ dobInputID, dobDefaultValue, isEditing, - shouldShowHelpLinks = true, + footerComponent, }: DateOfBirthStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -96,7 +95,7 @@ function DateOfBirthStep({ maxDate={maxDate} shouldSaveDraft={!isEditing} /> - {shouldShowHelpLinks && } + {footerComponent} ); } diff --git a/src/components/SubStepForms/SingleFieldStep.tsx b/src/components/SubStepForms/SingleFieldStep.tsx index 7ab709e3d5c1..be9b3c033f96 100644 --- a/src/components/SubStepForms/SingleFieldStep.tsx +++ b/src/components/SubStepForms/SingleFieldStep.tsx @@ -75,7 +75,7 @@ function SingleFieldStep({ submitButtonStyles={[styles.mb0]} > - {formTitle} + {formTitle} {!!formDisclaimer && {formDisclaimer}} ({ defaultValue={defaultValue} maxLength={maxLength} shouldSaveDraft={!isEditing} + autoFocus /> {shouldShowHelpLinks && } diff --git a/src/components/SubStepForms/YesNoStep.tsx b/src/components/SubStepForms/YesNoStep.tsx new file mode 100644 index 000000000000..8e1f26e30011 --- /dev/null +++ b/src/components/SubStepForms/YesNoStep.tsx @@ -0,0 +1,73 @@ +import React, {useMemo, useState} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import FormProvider from '@components/Form/FormProvider'; +import type {Choice} from '@components/RadioButtons'; +import RadioButtons from '@components/RadioButtons'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type YesNoStepProps = { + /** The title of the question */ + title: string; + + /** The description of the question */ + description: string; + + /** The default value of the radio button */ + defaultValue: boolean; + + /** Callback when the value is selected */ + onSelectedValue: (value: boolean) => void; + + /** The style of the submit button */ + submitButtonStyles?: StyleProp; +}; + +function YesNoStep({title, description, defaultValue, onSelectedValue, submitButtonStyles}: YesNoStepProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const [value, setValue] = useState(defaultValue); + + const handleSubmit = () => { + onSelectedValue(value); + }; + const handleSelectValue = (newValue: string) => setValue(newValue === 'true'); + const options = useMemo( + () => [ + { + label: translate('common.yes'), + value: 'true', + }, + { + label: translate('common.no'), + value: 'false', + }, + ], + [translate], + ); + + return ( + + {title} + {description} + + + ); +} + +YesNoStep.displayName = 'YesNoStep'; + +export default YesNoStep; diff --git a/src/components/ValidateCode/ValidateCodeModal.tsx b/src/components/ValidateCode/ValidateCodeModal.tsx index 1e42773c2dc2..089416267b32 100644 --- a/src/components/ValidateCode/ValidateCodeModal.tsx +++ b/src/components/ValidateCode/ValidateCodeModal.tsx @@ -1,78 +1,83 @@ import React, {useCallback} from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import * as ValidationUtils from '@libs/ValidationUtils'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Session as SessionType} from '@src/types/onyx'; -type ValidateCodeModalOnyxProps = { - /** Session of currently logged in user */ - session: OnyxEntry; -}; - -type ValidateCodeModalProps = ValidateCodeModalOnyxProps & { +type ValidateCodeModalProps = { /** Code to display. */ code: string; /** The ID of the account to which the code belongs. */ accountID: number; }; -function ValidateCodeModal({code, accountID, session = {}}: ValidateCodeModalProps) { +function ValidateCodeModal({code, accountID}: ValidateCodeModalProps) { const theme = useTheme(); const styles = useThemeStyles(); + const [session] = useOnyx(ONYXKEYS.SESSION); const signInHere = useCallback(() => Session.signInWithValidateCode(accountID, code), [accountID, code]); const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); return ( - - - + { + Navigation.goBack(); + }} + > + + + + + + {translate('validateCodeModal.title')} + + + {translate('validateCodeModal.description')} + {!session?.authToken && ( + <> + {translate('validateCodeModal.or')} {translate('validateCodeModal.signInHere')} + + )} + . + + + + {code} + + + - {translate('validateCodeModal.title')} - - - {translate('validateCodeModal.description')} - {!session?.authToken && ( - <> - {translate('validateCodeModal.or')} {translate('validateCodeModal.signInHere')} - - )} - . - - - - {code} - - - - - + ); } ValidateCodeModal.displayName = 'ValidateCodeModal'; -export default withOnyx({ - session: {key: ONYXKEYS.SESSION}, -})(ValidateCodeModal); +export default ValidateCodeModal; diff --git a/src/components/VideoPopoverMenu/index.tsx b/src/components/VideoPopoverMenu/index.tsx index 23f3447cf495..f4f3092df109 100644 --- a/src/components/VideoPopoverMenu/index.tsx +++ b/src/components/VideoPopoverMenu/index.tsx @@ -35,6 +35,7 @@ function VideoPopoverMenu({ anchorPosition={anchorPosition} menuItems={menuItems} anchorRef={videoPlayerMenuRef} + shouldUseScrollView /> ); } diff --git a/src/languages/en.ts b/src/languages/en.ts index 910d4397d0a0..1a703f1bea1b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -41,6 +41,7 @@ import type { CharacterLimitParams, CompanyCardBankName, CompanyCardFeedNameParams, + CompanyNameParams, ConfirmThatParams, ConnectionNameParams, ConnectionParams, @@ -1929,6 +1930,7 @@ const translations = { website: 'Please enter a valid website.', zipCode: `Please enter a valid ZIP code using the format: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}.`, phoneNumber: 'Please enter a valid phone number.', + email: 'Please enter a valid email address.', companyName: 'Please enter a valid business name.', addressCity: 'Please enter a valid city.', addressStreet: 'Please enter a valid street address.', @@ -1950,6 +1952,7 @@ const translations = { lastName: 'Please enter a valid last name.', noDefaultDepositAccountOrDebitCardAvailable: 'Please add a default deposit account or debit card.', validationAmounts: 'The validation amounts you entered are incorrect. Please double check your bank statement and try again.', + fullName: 'Please enter a valid full name.', }, }, addPersonalBankAccountPage: { @@ -2303,6 +2306,26 @@ const translations = { }, signerInfoStep: { signerInfo: 'Signer info', + areYouDirector: ({companyName}: CompanyNameParams) => `Are you a director or senior officer at ${companyName}?`, + regulationRequiresUs: 'Regulation requires us to verify if the signer has the authority to take this action on behalf of the business.', + whatsYourName: "What's your legal name", + fullName: 'Legal full name', + whatsYourJobTitle: "What's your job title?", + jobTitle: 'Job title', + whatsYourDOB: "What's your date of birth?", + uploadID: 'Upload ID and proof of address', + id: "ID (driver's license or passport)", + personalAddress: 'Proof of personal address (e.g. utility bill)', + letsDoubleCheck: 'Let鈥檚 double check that everything looks right.', + legalName: 'Legal name', + proofOf: 'Proof of personal address', + enterOneEmail: 'Enter the email of director or senior officer at', + regulationRequiresOneMoreDirector: 'Regulation requires one more director or senior officer as a signer.', + hangTight: 'Hang tight...', + enterTwoEmails: 'Enter the emails of two directors or senior officers at', + sendReminder: 'Send a reminder', + chooseFile: 'Choose file', + weAreWaiting: "We're waiting for others to verify their identities as directors or senior officers of the business.", }, agreementsStep: { agreements: 'Agreements', diff --git a/src/languages/es.ts b/src/languages/es.ts index 0944c3c638a1..2bb66cec6548 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -40,6 +40,7 @@ import type { CharacterLimitParams, CompanyCardBankName, CompanyCardFeedNameParams, + CompanyNameParams, ConfirmThatParams, ConnectionNameParams, ConnectionParams, @@ -1950,6 +1951,7 @@ const translations = { website: 'Por favor, introduce un sitio web v谩lido.', zipCode: `Formato de c贸digo postal incorrecto. Formato aceptable: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}.`, phoneNumber: 'Por favor, introduce un tel茅fono v谩lido.', + email: 'Por favor, introduce una direcci贸n de correo electr贸nico v谩lida.', companyName: 'Por favor, introduce un nombre comercial legal v谩lido.', addressCity: 'Por favor, introduce una ciudad v谩lida.', addressStreet: 'Por favor, introduce una direcci贸n v谩lida que no sea un apartado postal.', @@ -1972,6 +1974,7 @@ const translations = { lastName: 'Por favor, introduce los apellidos.', noDefaultDepositAccountOrDebitCardAvailable: 'Por favor, a帽ade una cuenta bancaria para dep贸sitos o una tarjeta de d茅bito.', validationAmounts: 'Los importes de validaci贸n que introduciste son incorrectos. Por favor, comprueba tu cuenta bancaria e int茅ntalo de nuevo.', + fullName: 'Please enter a valid full name.', }, }, addPersonalBankAccountPage: { @@ -2328,6 +2331,26 @@ const translations = { }, signerInfoStep: { signerInfo: 'Informaci贸n del firmante', + areYouDirector: ({companyName}: CompanyNameParams) => `驴Es usted director o alto funcionario de ${companyName}?`, + regulationRequiresUs: 'La regulaci贸n requiere que verifiquemos si el firmante tiene la autoridad para realizar esta acci贸n en nombre de la empresa.', + whatsYourName: '驴Cu谩l es tu nombre legal?', + fullName: 'Nombre legal completo', + whatsYourJobTitle: '驴Cu谩l es tu puesto de trabajo?', + jobTitle: 'T铆tulo profesional', + whatsYourDOB: '驴Cual es tu fecha de nacimiento?', + uploadID: 'Subir documento de identidad y prueba de domicilio', + id: 'Identificaci贸n (licencia de conducir o pasaporte)', + personalAddress: 'Prueba de domicilio personal (por ejemplo, factura de servicios p煤blicos)', + letsDoubleCheck: 'Vamos a comprobar que todo est谩 bien.', + legalName: 'Nombre legal', + proofOf: 'Comprobante de domicilio personal', + enterOneEmail: 'Introduce el correo electr贸nico del director o alto funcionario en', + regulationRequiresOneMoreDirector: 'El reglamento exige que haya otro director o funcionario superior como firmante.', + hangTight: 'Espera un momento...', + enterTwoEmails: 'Introduce los correos electr贸nicos de dos directores o altos funcionarios en', + sendReminder: 'Enviar un recordatorio', + chooseFile: 'Seleccionar archivo', + weAreWaiting: 'Estamos esperando que otros verifiquen sus identidades como directores o altos funcionarios de la empresa.', }, agreementsStep: { agreements: 'Acuerdos', diff --git a/src/languages/params.ts b/src/languages/params.ts index 7574fe96bd60..87a322775cca 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -555,6 +555,10 @@ type CurrencyCodeParams = { currencyCode: string; }; +type CompanyNameParams = { + companyName: string; +}; + export type { AuthenticationErrorParams, ImportMembersSuccessfullDescriptionParams, @@ -756,4 +760,5 @@ export type { AssignCardParams, ImportedTypesParams, CurrencyCodeParams, + CompanyNameParams, }; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ee9b303cb7d4..fa3eea80c09c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -7945,6 +7945,30 @@ function getOptimisticDataForParentReportAction(reportID: string, lastVisibleAct }); } +function getQuickActionDetails( + quickActionReport: Report, + personalDetails: PersonalDetailsList | undefined, + policyChatForActivePolicy: Report | undefined, + reportNameValuePairs: ReportNameValuePairs, +): {quickActionAvatars: Icon[]; hideQABSubtitle: boolean} { + const isValidQuickActionReport = !(isEmptyObject(quickActionReport) || isArchivedRoom(quickActionReport, reportNameValuePairs)); + let hideQABSubtitle = false; + let quickActionAvatars: Icon[] = []; + if (isValidQuickActionReport) { + const avatars = getIcons(quickActionReport, personalDetails); + quickActionAvatars = avatars.length <= 1 || isPolicyExpenseChat(quickActionReport) ? avatars : avatars.filter((avatar) => avatar.id !== currentUserAccountID); + } else { + hideQABSubtitle = true; + } + if (!isEmptyObject(policyChatForActivePolicy)) { + quickActionAvatars = getIcons(policyChatForActivePolicy, personalDetails); + } + return { + quickActionAvatars, + hideQABSubtitle, + }; +} + function canBeAutoReimbursed(report: OnyxInputOrEntry, policy: OnyxInputOrEntry): boolean { if (isEmptyObject(policy)) { return false; @@ -8567,6 +8591,7 @@ export { getInvoicePayerName, getInvoicesChatName, getPayeeName, + getQuickActionDetails, hasActionsWithErrors, hasAutomatedExpensifyAccountIDs, hasExpensifyGuidesEmails, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 498de39a145d..e7d4a6328dfb 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7375,9 +7375,11 @@ function completePaymentOnboarding(paymentSelected: ValueOf, full = true) { if (chatReport.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(chatReport.policyID)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(chatReport.policyID)); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index ec87dcb16df8..32c0a40876d7 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3478,6 +3478,7 @@ function prepareOnboardingOptimisticData( adminsChatReportID?: string, onboardingPolicyID?: string, userReportedIntegration?: OnboardingAccounting, + wasInvited?: boolean, ) { // If the user has the "combinedTrackSubmit" beta enabled we'll show different tasks for track and submit expense. if (Permissions.canUseCombinedTrackSubmit()) { @@ -3528,7 +3529,11 @@ function prepareOnboardingOptimisticData( } const tasksData = data.tasks .filter((task) => { - if (task.type === 'addAccountingIntegration' && !userReportedIntegration) { + if (['setupCategories', 'setupTags'].includes(task.type) && userReportedIntegration) { + return false; + } + + if (['addAccountingIntegration', 'setupCategoriesAndTags'].includes(task.type) && !userReportedIntegration) { return false; } return true; @@ -3544,6 +3549,7 @@ function prepareOnboardingOptimisticData( navatticURL: getNavatticURL(environment, engagementChoice), integrationName, workspaceAccountingLink: `${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(onboardingPolicyID ?? '-1')}`, + workspaceSettingsLink: `${environmentURL}/${ROUTES.WORKSPACE_INITIAL.getRoute(onboardingPolicyID ?? '-1')}`, }) : task.description; const taskTitle = @@ -3756,12 +3762,14 @@ function prepareOnboardingOptimisticData( key: ONYXKEYS.NVP_INTRO_SELECTED, value: {choice: engagementChoice}, }, - { + ); + if (!wasInvited) { + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_ONBOARDING, value: {hasCompletedGuidedSetupFlow: true}, - }, - ); + }); + } const successData: OnyxUpdate[] = [...tasksForSuccessData]; successData.push({ @@ -3816,12 +3824,15 @@ function prepareOnboardingOptimisticData( key: ONYXKEYS.NVP_INTRO_SELECTED, value: {choice: null}, }, - { + ); + + if (!wasInvited) { + failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_ONBOARDING, value: {hasCompletedGuidedSetupFlow: false}, - }, - ); + }); + } if (userReportedIntegration) { optimisticData.push({ @@ -3905,6 +3916,7 @@ function completeOnboarding( paymentSelected?: string, companySize?: OnboardingCompanySize, userReportedIntegration?: OnboardingAccounting, + wasInvited?: boolean, ) { const {optimisticData, successData, failureData, guidedSetupData, actorAccountID} = prepareOnboardingOptimisticData( engagementChoice, @@ -3912,6 +3924,7 @@ function completeOnboarding( adminsChatReportID, onboardingPolicyID, userReportedIntegration, + wasInvited, ); const parameters: CompleteGuidedSetupParams = { diff --git a/src/pages/MissingPersonalDetails/substeps/DateOfBirth.tsx b/src/pages/MissingPersonalDetails/substeps/DateOfBirth.tsx index f00fb912cce5..84788d0458d0 100644 --- a/src/pages/MissingPersonalDetails/substeps/DateOfBirth.tsx +++ b/src/pages/MissingPersonalDetails/substeps/DateOfBirth.tsx @@ -28,7 +28,6 @@ function DateOfBirth({isEditing, onNext, onMove, personalDetailsValues}: CustomS stepFields={STEP_FIELDS} dobInputID={INPUT_IDS.DATE_OF_BIRTH} dobDefaultValue={personalDetailsValues[INPUT_IDS.DATE_OF_BIRTH]} - shouldShowHelpLinks={false} /> ); } diff --git a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerCheckUBO.tsx b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerCheckUBO.tsx index 438551cf4044..478642416e30 100644 --- a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerCheckUBO.tsx +++ b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerCheckUBO.tsx @@ -1,11 +1,7 @@ -import React, {useMemo, useState} from 'react'; -import FormProvider from '@components/Form/FormProvider'; -import type {Choice} from '@components/RadioButtons'; -import RadioButtons from '@components/RadioButtons'; -import Text from '@components/Text'; +import React from 'react'; +import YesNoStep from '@components/SubStepForms/YesNoStep'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import ONYXKEYS from '@src/ONYXKEYS'; type BeneficialOwnerCheckUBOProps = { /** The title of the question */ @@ -21,43 +17,15 @@ type BeneficialOwnerCheckUBOProps = { function BeneficialOwnerCheckUBO({title, onSelectedValue, defaultValue}: BeneficialOwnerCheckUBOProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [value, setValue] = useState(defaultValue); - - const handleSubmit = () => { - onSelectedValue(value); - }; - const handleSelectUBOValue = (newValue: string) => setValue(newValue === 'true'); - const options = useMemo( - () => [ - { - label: translate('common.yes'), - value: 'true', - }, - { - label: translate('common.no'), - value: 'false', - }, - ], - [translate], - ); return ( - - {title} - {translate('beneficialOwnerInfoStep.regulationRequiresUsToVerifyTheIdentity')} - - + /> ); } diff --git a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/DateOfBirthUBO.tsx b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/DateOfBirthUBO.tsx index c2cd95784596..8cd94653909a 100644 --- a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/DateOfBirthUBO.tsx +++ b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/DateOfBirthUBO.tsx @@ -37,7 +37,6 @@ function DateOfBirthUBO({onNext, onMove, isEditing, beneficialOwnerBeingModified stepFields={[dobInputID]} dobInputID={dobInputID} dobDefaultValue={dobDefaultValue} - shouldShowHelpLinks={false} /> ); } diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/DirectorCheck.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/DirectorCheck.tsx new file mode 100644 index 000000000000..7535f72a3970 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/DirectorCheck.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import YesNoStep from '@components/SubStepForms/YesNoStep'; +import useLocalize from '@hooks/useLocalize'; + +type DirectorCheckProps = { + /** The title of the question */ + title: string; + + /** The default value of the radio button */ + defaultValue: boolean; + + /** Callback when the value is selected */ + onSelectedValue: (value: boolean) => void; +}; + +function DirectorCheck({title, onSelectedValue, defaultValue}: DirectorCheckProps) { + const {translate} = useLocalize(); + + return ( + + ); +} + +DirectorCheck.displayName = 'DirectorCheck'; + +export default DirectorCheck; diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/EnterEmail.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/EnterEmail.tsx new file mode 100644 index 000000000000..5d0de4eb7fd9 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/EnterEmail.tsx @@ -0,0 +1,85 @@ +import {Str} from 'expensify-common'; +import React, {useCallback} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +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/ReimbursementAccountForm'; + +type EnterEmailProps = { + /** Callback when the form is submitted */ + onSubmit: () => void; + + /** Whether the user is a director */ + isUserDirector: boolean; +}; + +const {SIGNER_EMAIL, SECOND_SIGNER_EMAIL} = INPUT_IDS.ADDITIONAL_DATA.CORPAY; + +function EnterEmail({onSubmit, isUserDirector}: EnterEmailProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + const policyID = reimbursementAccount?.achData?.policyID ?? '-1'; + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const currency = policy?.outputCurrency ?? ''; + const shouldGatherBothEmails = currency === CONST.CURRENCY.AUD && !isUserDirector; + + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, shouldGatherBothEmails ? [SIGNER_EMAIL, SECOND_SIGNER_EMAIL] : [SIGNER_EMAIL]); + if (values[SIGNER_EMAIL] && !Str.isValidEmail(values[SIGNER_EMAIL])) { + errors[SIGNER_EMAIL] = translate('bankAccount.error.email'); + } + + if (shouldGatherBothEmails && values[SECOND_SIGNER_EMAIL] && !Str.isValidEmail(values[SECOND_SIGNER_EMAIL])) { + errors[SECOND_SIGNER_EMAIL] = translate('bankAccount.error.email'); + } + + return errors; + }, + [shouldGatherBothEmails, translate], + ); + + return ( + + {translate(shouldGatherBothEmails ? 'signerInfoStep.enterTwoEmails' : 'signerInfoStep.enterOneEmail')} + + {shouldGatherBothEmails && ( + + )} + + ); +} + +EnterEmail.displayName = 'EnterEmail'; + +export default EnterEmail; diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/HangTight.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/HangTight.tsx new file mode 100644 index 000000000000..15fea5e46691 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/HangTight.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; +import SafeAreaConsumer from '@components/SafeAreaConsumer'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; + +function HangTight({tempSubmit}: {tempSubmit: () => void}) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const handleSendReminder = () => { + // TODO remove that + tempSubmit(); + }; + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + + + + + + {translate('signerInfoStep.hangTight')} + {translate('signerInfoStep.weAreWaiting')} + + +