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')}
+
+
+
+
+
+ )}
+
+ );
+}
+
+HangTight.displayName = 'HangTight';
+
+export default HangTight;
diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/SignerInfo.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/SignerInfo.tsx
deleted file mode 100644
index 8e794f1f2f38..000000000000
--- a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/SignerInfo.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react';
-import {View} from 'react-native';
-import Button from '@components/Button';
-import InteractiveStepWrapper from '@components/InteractiveStepWrapper';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import CONST from '@src/CONST';
-
-type SignerInfoProps = {
- /** Handles back button press */
- onBackButtonPress: () => void;
-
- /** Handles submit button press */
- onSubmit: () => void;
-};
-
-function SignerInfo({onBackButtonPress, onSubmit}: SignerInfoProps) {
- const {translate} = useLocalize();
- const styles = useThemeStyles();
-
- return (
-
-
-
-
-
- );
-}
-
-SignerInfo.displayName = 'SignerInfo';
-
-export default SignerInfo;
diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/index.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/index.tsx
new file mode 100644
index 000000000000..363e444c5641
--- /dev/null
+++ b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/index.tsx
@@ -0,0 +1,152 @@
+import type {ComponentType} from 'react';
+import React, {useState} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import InteractiveStepWrapper from '@components/InteractiveStepWrapper';
+import useLocalize from '@hooks/useLocalize';
+import useSubStep from '@hooks/useSubStep';
+import type {SubStepProps} from '@hooks/useSubStep/types';
+import Navigation from '@navigation/Navigation';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
+import DirectorCheck from './DirectorCheck';
+import EnterEmail from './EnterEmail';
+import HangTight from './HangTight';
+import Confirmation from './substeps/Confirmation';
+import DateOfBirth from './substeps/DateOfBirth';
+import JobTitle from './substeps/JobTitle';
+import Name from './substeps/Name';
+import UploadDocuments from './substeps/UploadDocuments';
+
+type SignerInfoProps = {
+ /** Handles back button press */
+ onBackButtonPress: () => void;
+
+ /** Handles submit button press */
+ onSubmit: () => void;
+};
+
+type SignerDetailsFormProps = SubStepProps & {isSecondSigner: boolean};
+
+// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
+const SUBSTEP: Record = CONST.NON_USD_BANK_ACCOUNT.SIGNER_INFO_STEP.SUBSTEP;
+const {OWNS_MORE_THAN_25_PERCENT, COMPANY_NAME} = INPUT_IDS.ADDITIONAL_DATA.CORPAY;
+
+const bodyContent: Array> = [Name, JobTitle, DateOfBirth, UploadDocuments, Confirmation];
+const userIsOwnerBodyContent: Array> = [JobTitle, UploadDocuments, Confirmation];
+
+function SignerInfo({onBackButtonPress, onSubmit}: SignerInfoProps) {
+ const {translate} = useLocalize();
+
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+ const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
+ const policyID = reimbursementAccount?.achData?.policyID ?? '-1';
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
+ const currency = policy?.outputCurrency ?? '';
+ // TODO set this based on param from redirect or BE response
+ const isSecondSigner = false;
+ const isUserOwner = reimbursementAccount?.achData?.additionalData?.corpay?.[OWNS_MORE_THAN_25_PERCENT] ?? reimbursementAccountDraft?.[OWNS_MORE_THAN_25_PERCENT] ?? false;
+ const companyName = reimbursementAccount?.achData?.additionalData?.corpay?.[COMPANY_NAME] ?? reimbursementAccountDraft?.[COMPANY_NAME] ?? '';
+
+ const [currentSubStep, setCurrentSubStep] = useState(SUBSTEP.IS_DIRECTOR);
+ const [isUserDirector, setIsUserDirector] = useState(false);
+
+ const submit = () => {
+ if (currency === CONST.CURRENCY.AUD) {
+ setCurrentSubStep(SUBSTEP.ENTER_EMAIL);
+ } else {
+ onSubmit();
+ }
+ };
+
+ const handleNextSubStep = (value: boolean) => {
+ if (currentSubStep !== SUBSTEP.IS_DIRECTOR) {
+ return;
+ }
+
+ // user is director
+ if (value) {
+ setIsUserDirector(value);
+ setCurrentSubStep(SUBSTEP.SIGNER_DETAILS_FORM);
+ return;
+ }
+
+ setIsUserDirector(value);
+ setCurrentSubStep(SUBSTEP.ENTER_EMAIL);
+ };
+
+ const {
+ componentToRender: SignerDetailsForm,
+ isEditing,
+ screenIndex,
+ nextScreen,
+ prevScreen,
+ moveTo,
+ goToTheLastStep,
+ } = useSubStep({bodyContent: isUserOwner ? userIsOwnerBodyContent : bodyContent, startFrom: 0, onFinished: submit});
+
+ const handleBackButtonPress = () => {
+ if (isEditing) {
+ goToTheLastStep();
+ return;
+ }
+
+ if (currentSubStep === SUBSTEP.IS_DIRECTOR) {
+ onBackButtonPress();
+ } else if (currentSubStep === SUBSTEP.ENTER_EMAIL && isUserDirector) {
+ setCurrentSubStep(SUBSTEP.SIGNER_DETAILS_FORM);
+ } else if (currentSubStep === SUBSTEP.SIGNER_DETAILS_FORM && screenIndex > 0) {
+ prevScreen();
+ } else if (currentSubStep === SUBSTEP.SIGNER_DETAILS_FORM && screenIndex === 0) {
+ setCurrentSubStep(SUBSTEP.IS_DIRECTOR);
+ } else if (currentSubStep === SUBSTEP.HANG_TIGHT) {
+ Navigation.goBack();
+ } else {
+ setCurrentSubStep((subStep) => subStep - 1);
+ }
+ };
+
+ const handleEmailSubmit = () => {
+ setCurrentSubStep(SUBSTEP.HANG_TIGHT);
+ };
+
+ return (
+
+ {currentSubStep === SUBSTEP.IS_DIRECTOR && (
+
+ )}
+
+ {currentSubStep === SUBSTEP.SIGNER_DETAILS_FORM && (
+
+ )}
+
+ {currentSubStep === SUBSTEP.ENTER_EMAIL && (
+
+ )}
+
+ {currentSubStep === SUBSTEP.HANG_TIGHT && }
+
+ );
+}
+
+SignerInfo.displayName = 'SignerInfo';
+
+export default SignerInfo;
diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/Confirmation.tsx
new file mode 100644
index 000000000000..56966bc45887
--- /dev/null
+++ b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/Confirmation.tsx
@@ -0,0 +1,99 @@
+import React, {useMemo} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import ConfirmationStep from '@components/SubStepForms/ConfirmationStep';
+import useLocalize from '@hooks/useLocalize';
+import type {SubStepProps} from '@hooks/useSubStep/types';
+import getSubstepValues from '@pages/ReimbursementAccount/utils/getSubstepValues';
+import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
+
+type ConfirmationProps = SubStepProps & {isSecondSigner: boolean};
+
+const SINGER_INFO_STEP_KEYS = INPUT_IDS.ADDITIONAL_DATA.CORPAY;
+const {
+ SIGNER_FULL_NAME,
+ SECOND_SIGNER_FULL_NAME,
+ SIGNER_JOB_TITLE,
+ SECOND_SIGNER_JOB_TITLE,
+ SIGNER_DATE_OF_BIRTH,
+ SECOND_SIGNER_DATE_OF_BIRTH,
+ SIGNER_COPY_OF_ID,
+ SECOND_SIGNER_COPY_OF_ID,
+ SIGNER_ADDRESS_PROOF,
+ SECOND_SIGNER_ADDRESS_PROOF,
+ OWNS_MORE_THAN_25_PERCENT,
+} = INPUT_IDS.ADDITIONAL_DATA.CORPAY;
+
+function Confirmation({onNext, onMove, isEditing, isSecondSigner}: ConfirmationProps) {
+ const {translate} = useLocalize();
+
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+ const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
+ const isUserOwner = reimbursementAccount?.achData?.additionalData?.corpay?.[OWNS_MORE_THAN_25_PERCENT] ?? reimbursementAccountDraft?.[OWNS_MORE_THAN_25_PERCENT] ?? false;
+ const values = useMemo(() => getSubstepValues(SINGER_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]);
+
+ const IDs = values[isSecondSigner ? SECOND_SIGNER_COPY_OF_ID : SIGNER_COPY_OF_ID];
+ const proofs = values[isSecondSigner ? SECOND_SIGNER_ADDRESS_PROOF : SIGNER_ADDRESS_PROOF];
+
+ const summaryItems = [
+ {
+ title: values[isSecondSigner ? SECOND_SIGNER_JOB_TITLE : SIGNER_JOB_TITLE],
+ description: translate('signerInfoStep.jobTitle'),
+ shouldShowRightIcon: true,
+ onPress: () => {
+ onMove(1);
+ },
+ },
+ {
+ title: IDs ? IDs.map((id) => id.name).join(', ') : '',
+ description: translate('signerInfoStep.id'),
+ shouldShowRightIcon: true,
+ onPress: () => {
+ onMove(3);
+ },
+ },
+ {
+ title: proofs ? proofs.map((proof) => proof.name).join(', ') : '',
+ description: translate('signerInfoStep.proofOf'),
+ shouldShowRightIcon: true,
+ onPress: () => {
+ onMove(3);
+ },
+ },
+ ];
+
+ if (!isUserOwner) {
+ summaryItems.unshift({
+ title: `${values[isSecondSigner ? SECOND_SIGNER_FULL_NAME : SIGNER_FULL_NAME]}`,
+ description: translate('signerInfoStep.legalName'),
+ shouldShowRightIcon: true,
+ onPress: () => {
+ onMove(0);
+ },
+ });
+
+ summaryItems.splice(2, 0, {
+ title: values[isSecondSigner ? SECOND_SIGNER_DATE_OF_BIRTH : SIGNER_DATE_OF_BIRTH],
+ description: translate('common.dob'),
+ shouldShowRightIcon: true,
+ onPress: () => {
+ onMove(2);
+ },
+ });
+ }
+
+ return (
+
+ );
+}
+
+Confirmation.displayName = 'Confirmation';
+
+export default Confirmation;
diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/DateOfBirth.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/DateOfBirth.tsx
new file mode 100644
index 000000000000..ba83edd08d97
--- /dev/null
+++ b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/DateOfBirth.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import {useOnyx} from 'react-native-onyx';
+import DateOfBirthStep from '@components/SubStepForms/DateOfBirthStep';
+import useLocalize from '@hooks/useLocalize';
+import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit';
+import type {SubStepProps} from '@hooks/useSubStep/types';
+import useThemeStyles from '@hooks/useThemeStyles';
+import WhyLink from '@pages/ReimbursementAccount/NonUSD/WhyLink';
+import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
+
+type DateOfBirthProps = SubStepProps & {isSecondSigner: boolean};
+
+const {SIGNER_DATE_OF_BIRTH, SECOND_SIGNER_DATE_OF_BIRTH} = INPUT_IDS.ADDITIONAL_DATA.CORPAY;
+
+function DateOfBirth({onNext, onMove, isEditing, isSecondSigner}: DateOfBirthProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ const inputID = isSecondSigner ? SECOND_SIGNER_DATE_OF_BIRTH : SIGNER_DATE_OF_BIRTH;
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+ const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
+ const defaultValue = reimbursementAccount?.achData?.additionalData?.corpay?.[inputID] ?? reimbursementAccountDraft?.[inputID] ?? '';
+
+ const handleSubmit = useReimbursementAccountStepFormSubmit({
+ fieldIds: [inputID],
+ onNext,
+ shouldSaveDraft: isEditing,
+ });
+
+ return (
+
+ isEditing={isEditing}
+ onNext={onNext}
+ onMove={onMove}
+ formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM}
+ formTitle={translate('signerInfoStep.whatsYourDOB')}
+ onSubmit={handleSubmit}
+ stepFields={[inputID]}
+ dobInputID={inputID}
+ dobDefaultValue={defaultValue}
+ footerComponent={}
+ />
+ );
+}
+
+DateOfBirth.displayName = 'DateOfBirth';
+
+export default DateOfBirth;
diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/JobTitle.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/JobTitle.tsx
new file mode 100644
index 000000000000..30da9f4f54ec
--- /dev/null
+++ b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/JobTitle.tsx
@@ -0,0 +1,58 @@
+import React, {useCallback} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import SingleFieldStep from '@components/SubStepForms/SingleFieldStep';
+import useLocalize from '@hooks/useLocalize';
+import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit';
+import type {SubStepProps} from '@hooks/useSubStep/types';
+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 JobTitleProps = SubStepProps & {isSecondSigner: boolean};
+
+const {SIGNER_JOB_TITLE, SECOND_SIGNER_JOB_TITLE} = INPUT_IDS.ADDITIONAL_DATA.CORPAY;
+
+function JobTitle({onNext, onMove, isEditing, isSecondSigner}: JobTitleProps) {
+ const {translate} = useLocalize();
+
+ const inputID = isSecondSigner ? SECOND_SIGNER_JOB_TITLE : SIGNER_JOB_TITLE;
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+ const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
+ const defaultValue = reimbursementAccount?.achData?.additionalData?.corpay?.[inputID] ?? reimbursementAccountDraft?.[inputID] ?? '';
+
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ return ValidationUtils.getFieldRequiredErrors(values, [inputID]);
+ },
+ [inputID],
+ );
+
+ const handleSubmit = useReimbursementAccountStepFormSubmit({
+ fieldIds: [inputID],
+ onNext,
+ shouldSaveDraft: isEditing,
+ });
+
+ return (
+
+ isEditing={isEditing}
+ onNext={onNext}
+ onMove={onMove}
+ formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM}
+ formTitle={translate('signerInfoStep.whatsYourJobTitle')}
+ validate={validate}
+ onSubmit={handleSubmit}
+ inputId={inputID}
+ inputLabel={translate('signerInfoStep.jobTitle')}
+ inputMode={CONST.INPUT_MODE.TEXT}
+ defaultValue={defaultValue}
+ shouldShowHelpLinks={false}
+ />
+ );
+}
+
+JobTitle.displayName = 'JobTitle';
+
+export default JobTitle;
diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/Name.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/Name.tsx
new file mode 100644
index 000000000000..97d3c1f8b27f
--- /dev/null
+++ b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/Name.tsx
@@ -0,0 +1,64 @@
+import React, {useCallback} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import SingleFieldStep from '@components/SubStepForms/SingleFieldStep';
+import useLocalize from '@hooks/useLocalize';
+import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit';
+import type {SubStepProps} from '@hooks/useSubStep/types';
+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 NameProps = SubStepProps & {isSecondSigner: boolean};
+
+const {SIGNER_FULL_NAME, SECOND_SIGNER_FULL_NAME} = INPUT_IDS.ADDITIONAL_DATA.CORPAY;
+
+function Name({onNext, onMove, isEditing, isSecondSigner}: NameProps) {
+ const {translate} = useLocalize();
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+ const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
+
+ const inputID = isSecondSigner ? SECOND_SIGNER_FULL_NAME : SIGNER_FULL_NAME;
+ const defaultValue = reimbursementAccount?.achData?.additionalData?.corpay?.[inputID] ?? reimbursementAccountDraft?.[inputID] ?? '';
+
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors = ValidationUtils.getFieldRequiredErrors(values, [inputID]);
+
+ if (values[inputID] && !ValidationUtils.isValidLegalName(values[inputID])) {
+ errors[inputID] = translate('bankAccount.error.fullName');
+ }
+
+ return errors;
+ },
+ [inputID, translate],
+ );
+
+ const handleSubmit = useReimbursementAccountStepFormSubmit({
+ fieldIds: [inputID],
+ onNext,
+ shouldSaveDraft: isEditing,
+ });
+
+ return (
+
+ isEditing={isEditing}
+ onNext={onNext}
+ onMove={onMove}
+ formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM}
+ formTitle={translate('signerInfoStep.whatsYourName')}
+ validate={validate}
+ onSubmit={handleSubmit}
+ inputId={inputID}
+ inputLabel={translate('signerInfoStep.fullName')}
+ inputMode={CONST.INPUT_MODE.TEXT}
+ defaultValue={defaultValue}
+ shouldShowHelpLinks={false}
+ />
+ );
+}
+
+Name.displayName = 'Name';
+
+export default Name;
diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/UploadDocuments.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/UploadDocuments.tsx
new file mode 100644
index 000000000000..ed47b4bf235e
--- /dev/null
+++ b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/substeps/UploadDocuments.tsx
@@ -0,0 +1,121 @@
+import React, {useCallback, useState} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import type {FileObject} from '@components/AttachmentModal';
+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 UploadFile from '@components/UploadFile';
+import useLocalize from '@hooks/useLocalize';
+import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit';
+import type {SubStepProps} from '@hooks/useSubStep/types';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as ValidationUtils from '@libs/ValidationUtils';
+import WhyLink from '@pages/ReimbursementAccount/NonUSD/WhyLink';
+import * as FormActions from '@userActions/FormActions';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
+
+type UploadDocumentsProps = SubStepProps;
+
+const {SIGNER_ADDRESS_PROOF, SIGNER_COPY_OF_ID} = INPUT_IDS.ADDITIONAL_DATA.CORPAY;
+const STEP_FIELDS = [SIGNER_COPY_OF_ID, SIGNER_ADDRESS_PROOF];
+
+function UploadDocuments({onNext, isEditing}: UploadDocumentsProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+ const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
+
+ const defaultValues = {
+ [SIGNER_COPY_OF_ID]: reimbursementAccount?.achData?.additionalData?.corpay?.[SIGNER_COPY_OF_ID] ?? reimbursementAccountDraft?.[SIGNER_COPY_OF_ID] ?? [],
+ [SIGNER_ADDRESS_PROOF]: reimbursementAccount?.achData?.additionalData?.corpay?.[SIGNER_ADDRESS_PROOF] ?? reimbursementAccountDraft?.[SIGNER_ADDRESS_PROOF] ?? [],
+ };
+
+ const [uploadedIDs, setUploadedID] = useState(defaultValues[SIGNER_ADDRESS_PROOF]);
+ const [uploadedProofsOfAddress, setUploadedProofOfAddress] = useState(defaultValues[SIGNER_ADDRESS_PROOF]);
+
+ const validate = useCallback((values: FormOnyxValues): FormInputErrors => {
+ return ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS);
+ }, []);
+
+ const handleSubmit = useReimbursementAccountStepFormSubmit({
+ fieldIds: STEP_FIELDS,
+ onNext,
+ shouldSaveDraft: isEditing,
+ });
+
+ const handleSelectIDFile = (files: FileObject[]) => {
+ FormActions.setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[SIGNER_COPY_OF_ID]: [...uploadedIDs, ...files]});
+ setUploadedID((prev) => [...prev, ...files]);
+ };
+
+ const handleRemoveIDFile = (fileUri: string) => {
+ const newUploadedIDs = uploadedIDs.filter((file) => file.uri !== fileUri);
+ FormActions.setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[SIGNER_COPY_OF_ID]: newUploadedIDs});
+ setUploadedID(newUploadedIDs);
+ };
+
+ const handleSelectProofOfAddressFile = (files: FileObject[]) => {
+ FormActions.setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[SIGNER_ADDRESS_PROOF]: [...uploadedProofsOfAddress, ...files]});
+ setUploadedProofOfAddress((prev) => [...prev, ...files]);
+ };
+
+ const handleRemoveProofOfAddressFile = (fileUri: string) => {
+ const newUploadedProofsOfAddress = uploadedProofsOfAddress.filter((file) => file.uri !== fileUri);
+ FormActions.setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[SIGNER_ADDRESS_PROOF]: newUploadedProofsOfAddress});
+ setUploadedProofOfAddress(newUploadedProofsOfAddress);
+ };
+
+ // TODO: check if this is necessary
+ const setError = (error: string) => {
+ // eslint-disable-next-line
+ console.info(error);
+ };
+
+ return (
+
+
+ {translate('signerInfoStep.uploadID')}
+ {translate('signerInfoStep.id')}
+
+ {translate('signerInfoStep.proofOf')}
+
+
+
+
+ );
+}
+
+UploadDocuments.displayName = 'UploadDocuments';
+
+export default UploadDocuments;
diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/DateOfBirth.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/DateOfBirth.tsx
index 526181a6cb84..f6e7b084e0eb 100644
--- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/DateOfBirth.tsx
+++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/DateOfBirth.tsx
@@ -4,6 +4,8 @@ import DateOfBirthStep from '@components/SubStepForms/DateOfBirthStep';
import useLocalize from '@hooks/useLocalize';
import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit';
import type {SubStepProps} from '@hooks/useSubStep/types';
+import useThemeStyles from '@hooks/useThemeStyles';
+import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
@@ -12,6 +14,7 @@ const STEP_FIELDS = [PERSONAL_INFO_DOB_KEY];
function DateOfBirth({onNext, onMove, isEditing}: SubStepProps) {
const {translate} = useLocalize();
+ const styles = useThemeStyles();
const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
@@ -35,6 +38,7 @@ function DateOfBirth({onNext, onMove, isEditing}: SubStepProps) {
stepFields={STEP_FIELDS}
dobInputID={PERSONAL_INFO_DOB_KEY}
dobDefaultValue={dobDefaultValue}
+ footerComponent={}
/>
);
}
diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx
index 29173817f5ac..6c3b289e59df 100644
--- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx
+++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx
@@ -46,7 +46,7 @@ import BeneficialOwnerInfo from './NonUSD/BeneficialOwnerInfo/BeneficialOwnerInf
import BusinessInfo from './NonUSD/BusinessInfo/BusinessInfo';
import Country from './NonUSD/Country/Country';
import Finish from './NonUSD/Finish/Finish';
-import SignerInfo from './NonUSD/SignerInfo/SignerInfo';
+import SignerInfo from './NonUSD/SignerInfo';
import RequestorStep from './RequestorStep';
type ReimbursementAccountPageProps = WithPolicyOnyxProps & StackScreenProps;
diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx
index f3a98089b2b1..969e1a90ec3b 100644
--- a/src/pages/home/HeaderView.tsx
+++ b/src/pages/home/HeaderView.tsx
@@ -1,3 +1,4 @@
+import {useRoute} from '@react-navigation/native';
import React, {memo} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
@@ -24,8 +25,7 @@ import usePolicy from '@hooks/usePolicy';
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 OptionsListUtils from '@libs/OptionsListUtils';
import Parser from '@libs/Parser';
import * as ReportUtils from '@libs/ReportUtils';
@@ -36,6 +36,7 @@ import * as Task from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
import type {Icon as IconType} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -67,6 +68,7 @@ const fallbackIcon: IconType = {
function HeaderView({report, parentReportAction, reportID, onNavigationMenuButtonClicked, shouldUseNarrowLayout = false}: HeaderViewProps) {
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
+ const route = useRoute();
const [isDeleteTaskConfirmModalVisible, setIsDeleteTaskConfirmModalVisible] = React.useState(false);
const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.invoiceReceiver && 'policyID' in report.invoiceReceiver ? report.invoiceReceiver.policyID : -1}`);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
@@ -147,7 +149,7 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto
const shouldUseGroupTitle = isGroupChat && (!!report?.reportName || !isMultipleParticipant);
const isLoading = !report?.reportID || !title;
- const isReportInRHP = isReportOpenInRHP(navigationRef?.getRootState());
+ const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP;
const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth;
const isChatUsedForOnboarding = ReportUtils.isChatUsedForOnboarding(report);
diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
index 247bdbf105c6..82985c38c2ae 100644
--- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
+++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
@@ -178,6 +178,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
const {canUseSpotnanaTravel, canUseCombinedTrackSubmit} = usePermissions();
const canSendInvoice = useMemo(() => PolicyUtils.canSendInvoice(allPolicies as OnyxCollection, session?.email), [allPolicies, session?.email]);
+ const isValidReport = !(isEmptyObject(quickActionReport) || ReportUtils.isArchivedRoom(quickActionReport, reportNameValuePairs));
const {environment} = useEnvironment();
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
const navatticURL = getNavatticURL(environment, introSelected?.choice);
@@ -202,7 +203,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
);
const quickActionAvatars = useMemo(() => {
- if (quickActionReport) {
+ if (isValidReport) {
const avatars = ReportUtils.getIcons(quickActionReport, personalDetails);
return avatars.length <= 1 || ReportUtils.isPolicyExpenseChat(quickActionReport) ? avatars : avatars.filter((avatar) => avatar.id !== session?.accountID);
}
@@ -237,7 +238,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
}, [quickAction, translate, quickActionAvatars, quickActionReport]);
const hideQABSubtitle = useMemo(() => {
- if (isEmptyObject(quickActionReport)) {
+ if (!isValidReport) {
return true;
}
if (quickActionAvatars.length === 0) {
@@ -245,7 +246,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
}
const displayName = personalDetails?.[quickActionAvatars.at(0)?.id ?? -1]?.firstName ?? '';
return quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && displayName.length === 0;
- }, [personalDetails, quickActionReport, quickAction?.action, quickActionAvatars]);
+ }, [isValidReport, quickActionAvatars, personalDetails, quickAction?.action]);
const selectOption = useCallback(
(onSelected: () => void, shouldRestrictAction: boolean) => {
@@ -259,7 +260,6 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
);
const navigateToQuickAction = useCallback(() => {
- const isValidReport = !(isEmptyObject(quickActionReport) || ReportUtils.isArchivedRoom(quickActionReport, reportNameValuePairs));
const quickActionReportID = isValidReport ? quickActionReport?.reportID ?? '-1' : ReportUtils.generateReportID();
switch (quickAction?.action) {
@@ -288,7 +288,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
break;
default:
}
- }, [quickAction, quickActionReport, reportNameValuePairs, selectOption]);
+ }, [isValidReport, quickAction?.action, quickAction?.targetAccountID, quickActionReport?.reportID, selectOption]);
/**
* Check if LHN status changed from active to inactive.
@@ -476,7 +476,6 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
onSelected: () =>
interceptAnonymousUser(() => {
selectOption(() => {
- const isValidReport = !(isEmptyObject(policyChatForActivePolicy) || ReportUtils.isArchivedRoom(policyChatForActivePolicy, reportNameValuePairs));
const quickActionReportID = isValidReport ? policyChatForActivePolicy?.reportID ?? '-1' : ReportUtils.generateReportID();
IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID ?? '-1', CONST.IOU.REQUEST_TYPE.SCAN, true);
}, true);
@@ -503,7 +502,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
quickActionReport,
navigateToQuickAction,
selectOption,
- reportNameValuePairs,
+ isValidReport,
]);
return (
diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx
index 9d6f167fc04e..8f69663fb936 100644
--- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx
+++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx
@@ -21,7 +21,7 @@ import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import {addSMSDomainIfPhoneNumber} from '@libs/PhoneNumber';
import * as UserUtils from '@libs/UserUtils';
-import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import * as User from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -109,72 +109,74 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) {
Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(navigateBackTo));
}, [navigateBackTo]);
+ if (isActingAsDelegate) {
+ return ;
+ }
+
return (
-
- loginInputRef.current?.focus()}
- includeSafeAreaPaddingBottom={false}
- shouldEnableMaxHeight
- testID={NewContactMethodPage.displayName}
+ loginInputRef.current?.focus()}
+ includeSafeAreaPaddingBottom={false}
+ shouldEnableMaxHeight
+ testID={NewContactMethodPage.displayName}
+ >
+
+
-
-
- {translate('common.pleaseEnterEmailOrPhoneNumber')}
-
-
-
- {hasFailedToSendVerificationCode && (
-
- )}
-
- {
- if (!loginData) {
- return;
- }
- User.clearContactMethodErrors(addSMSDomainIfPhoneNumber(pendingContactAction?.contactMethod ?? contactMethod), 'addedLogin');
- }}
- onClose={() => {
- if (loginData?.errorFields && pendingContactAction?.contactMethod) {
- User.clearContactMethod(pendingContactAction?.contactMethod);
- User.clearUnvalidatedNewContactMethodAction();
- }
- setIsValidateCodeActionModalVisible(false);
- }}
- isVisible={isValidateCodeActionModalVisible}
- hasMagicCodeBeenSent={!!loginData?.validateCodeSent}
- title={translate('delegate.makeSureItIsYou')}
- sendValidateCode={() => User.requestValidateCodeAction()}
- descriptionPrimary={translate('contacts.enterMagicCode', {contactMethod})}
- />
-
-
+ {translate('common.pleaseEnterEmailOrPhoneNumber')}
+
+
+
+ {hasFailedToSendVerificationCode && (
+
+ )}
+
+ {
+ if (!loginData) {
+ return;
+ }
+ User.clearContactMethodErrors(addSMSDomainIfPhoneNumber(pendingContactAction?.contactMethod ?? contactMethod), 'addedLogin');
+ }}
+ onClose={() => {
+ if (loginData?.errorFields && pendingContactAction?.contactMethod) {
+ User.clearContactMethod(pendingContactAction?.contactMethod);
+ User.clearUnvalidatedNewContactMethodAction();
+ }
+ setIsValidateCodeActionModalVisible(false);
+ }}
+ isVisible={isValidateCodeActionModalVisible}
+ hasMagicCodeBeenSent={!!loginData?.validateCodeSent}
+ title={translate('delegate.makeSureItIsYou')}
+ sendValidateCode={() => User.requestValidateCodeAction()}
+ descriptionPrimary={translate('contacts.enterMagicCode', {contactMethod})}
+ />
+
);
}
diff --git a/src/pages/signin/SignInPage.tsx b/src/pages/signin/SignInPage.tsx
index 5f00be36c018..1068cf97197e 100644
--- a/src/pages/signin/SignInPage.tsx
+++ b/src/pages/signin/SignInPage.tsx
@@ -1,7 +1,7 @@
import {Str} from 'expensify-common';
import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react';
-import type {ForwardedRef} from 'react';
-import {useOnyx} from 'react-native-onyx';
+import type {ForwardedRef, RefAttributes} from 'react';
+import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import ColorSchemeWrapper from '@components/ColorSchemeWrapper';
import CustomStatusBarAndBackground from '@components/CustomStatusBarAndBackground';
@@ -24,7 +24,7 @@ import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {Account, Credentials} from '@src/types/onyx';
+import type {Account, Credentials, Locale} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import ChooseSSOOrMagicCode from './ChooseSSOOrMagicCode';
import EmailDeliveryFailurePage from './EmailDeliveryFailurePage';
@@ -37,7 +37,21 @@ import UnlinkLoginForm from './UnlinkLoginForm';
import ValidateCodeForm from './ValidateCodeForm';
import type {BaseValidateCodeFormRef} from './ValidateCodeForm/BaseValidateCodeForm';
-type SignInPageInnerProps = {
+type SignInPageInnerOnyxProps = {
+ /** The details about the account that the user is signing in with */
+ account: OnyxEntry;
+
+ /** The credentials of the person signing in */
+ credentials: OnyxEntry;
+
+ /** Active Clients connected to ONYX Database */
+ activeClients: OnyxEntry;
+
+ /** The user's preferred locale */
+ preferredLocale: OnyxEntry;
+};
+
+type SignInPageInnerProps = SignInPageInnerOnyxProps & {
shouldEnableMaxHeight?: boolean;
};
@@ -132,18 +146,8 @@ function getRenderOptions({
shouldShouldSignUpWelcomeForm,
};
}
-function SignInPage({shouldEnableMaxHeight = true}: SignInPageInnerProps, ref: ForwardedRef) {
- const [credentials] = useOnyx(ONYXKEYS.CREDENTIALS);
- const [account] = useOnyx(ONYXKEYS.ACCOUNT);
- /**
- This variable is only added to make sure the component is re-rendered
- whenever the activeClients change, so that we call the
- ActiveClientManager.isClientTheLeader function
- everytime the leader client changes.
- We use that function to prevent repeating code that checks which client is the leader.
- */
- const [activeClients] = useOnyx(ONYXKEYS.ACTIVE_CLIENTS);
- const [preferredLocale] = useOnyx(ONYXKEYS.NVP_PREFERRED_LOCALE);
+
+function SignInPage({credentials, account, activeClients = [], preferredLocale, shouldEnableMaxHeight = true}: SignInPageInnerProps, ref: ForwardedRef) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate, formatPhoneNumber} = useLocalize();
@@ -291,6 +295,7 @@ function SignInPage({shouldEnableMaxHeight = true}: SignInPageInnerProps, ref: F
@@ -335,6 +340,7 @@ function SignInPage({shouldEnableMaxHeight = true}: SignInPageInnerProps, ref: F
}
type SignInPageProps = SignInPageInnerProps;
+type SignInPageOnyxProps = SignInPageInnerOnyxProps;
const SignInPageWithRef = forwardRef(SignInPage);
function SignInPageThemeWrapper(props: SignInPageProps, ref: ForwardedRef) {
@@ -356,6 +362,20 @@ function SignInPageThemeWrapper(props: SignInPageProps, ref: ForwardedRef, SignInPageOnyxProps>({
+ account: {key: ONYXKEYS.ACCOUNT},
+ credentials: {key: ONYXKEYS.CREDENTIALS},
+ /**
+ This variable is only added to make sure the component is re-rendered
+ whenever the activeClients change, so that we call the
+ ActiveClientManager.isClientTheLeader function
+ everytime the leader client changes.
+ We use that function to prevent repeating code that checks which client is the leader.
+ */
+ activeClients: {key: ONYXKEYS.ACTIVE_CLIENTS},
+ preferredLocale: {
+ key: ONYXKEYS.NVP_PREFERRED_LOCALE,
+ },
+})(forwardRef(SignInPageThemeWrapper));
export type {SignInPageRef};
diff --git a/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx b/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx
index a0231f20641b..e9aac5685dc1 100644
--- a/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx
+++ b/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx
@@ -45,8 +45,8 @@ function WorkspaceCardListHeader({policyID}: WorkspaceCardListHeaderProps) {
/>
-
-
+
+
)}
-
+
PersonalDetailsUtils.getDisplayNameOrDefault(cardholder), [cardholder]);
const cardType = isVirtual ? translate('workspace.expensifyCard.virtual') : translate('workspace.expensifyCard.physical');
return (
-
-
+
+
{cardType}
)}
-
+
- {CurrencyUtils.convertToDisplayString(limit, currency)}
+ {CurrencyUtils.convertToShortDisplayString(limit, currency)}
{shouldUseNarrowLayout && (
-
-
- {cardType}
-
-
+
+ {cardType}
+
)}
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
index b9948466bef9..77aaee13fb9e 100644
--- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
@@ -1,7 +1,7 @@
import {useFocusEffect} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useMemo, useState} from 'react';
-import {ActivityIndicator, View} from 'react-native';
+import {ActivityIndicator, InteractionManager, View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import ApprovalWorkflowSection from '@components/ApprovalWorkflowSection';
import ConfirmModal from '@components/ConfirmModal';
@@ -92,7 +92,9 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
useFocusEffect(
useCallback(() => {
- fetchData();
+ InteractionManager.runAfterInteractions(() => {
+ fetchData();
+ });
}, [fetchData]),
);
diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx
index d67dd564057c..b4f1164c8102 100644
--- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx
+++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx
@@ -50,8 +50,10 @@ function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true
// We need to remove members and approvers that are no longer in the updated workflow
const membersToRemove = initialApprovalWorkflow.members.filter((initialMember) => !approvalWorkflow.members.some((member) => member.email === initialMember.email));
const approversToRemove = initialApprovalWorkflow.approvers.filter((initialApprover) => !approvalWorkflow.approvers.some((approver) => approver.email === initialApprover.email));
- Workflow.updateApprovalWorkflow(route.params.policyID, approvalWorkflow, membersToRemove, approversToRemove);
Navigation.dismissModal();
+ InteractionManager.runAfterInteractions(() => {
+ Workflow.updateApprovalWorkflow(route.params.policyID, approvalWorkflow, membersToRemove, approversToRemove);
+ });
}, [approvalWorkflow, initialApprovalWorkflow, route.params.policyID]);
const removeApprovalWorkflow = useCallback(() => {
diff --git a/src/styles/index.ts b/src/styles/index.ts
index ade24ef9d5be..8ebfc5582b02 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -3125,9 +3125,9 @@ const styles = (theme: ThemeColors) =>
radioButtonContainer: {
backgroundColor: theme.componentBG,
- borderRadius: 10,
- height: 20,
- width: 20,
+ borderRadius: 14,
+ height: 28,
+ width: 28,
borderColor: theme.border,
borderWidth: 1,
justifyContent: 'center',
diff --git a/src/styles/utils/sizing.ts b/src/styles/utils/sizing.ts
index 0cf3efd54d61..c43ed2cfe655 100644
--- a/src/styles/utils/sizing.ts
+++ b/src/styles/utils/sizing.ts
@@ -96,6 +96,10 @@ export default {
width: '80%',
},
+ w90: {
+ width: '90%',
+ },
+
w100: {
width: '100%',
},
diff --git a/src/types/form/ReimbursementAccountForm.ts b/src/types/form/ReimbursementAccountForm.ts
index e76158244199..c3a5f34215ef 100644
--- a/src/types/form/ReimbursementAccountForm.ts
+++ b/src/types/form/ReimbursementAccountForm.ts
@@ -53,9 +53,19 @@ const INPUT_IDS = {
AMOUNT2: 'amount2',
AMOUNT3: 'amount3',
ADDITIONAL_DATA: {
+ ACCOUNT_HOLDER_NAME: 'accountHolderName',
+ ADDRESS_STREET: 'addressStreet',
+ ADDRESS_CITY: 'addressCity',
+ ADDRESS_STATE: 'addressState',
+ ADDRESS_ZIP_CODE: 'addressZipCode',
+ ACCOUNT_HOLDER_COUNTRY: 'accountHolderCountry',
COUNTRY: 'country',
CORPAY: {
ACCOUNT_HOLDER_COUNTRY: 'accountHolderCountry',
+ SWIFT_CODE: 'swiftCode',
+ BANK_NAME: 'bankName',
+ BANK_CITY: 'bankCity',
+ BANK_ADDRESS_LINE_1: 'bankAddress',
BANK_STATEMENT: 'bankStatement',
COMPANY_NAME: 'companyName',
COMPANY_STREET: 'companyStreet',
@@ -65,12 +75,50 @@ const INPUT_IDS = {
COMPANY_COUNTRY: 'companyCountry',
BUSINESS_CONTACT_NUMBER: 'businessContactNumber',
BUSINESS_CONFIRMATION_EMAIL: 'businessConfirmationEmail',
- BUSINESS_REGISTRATION_INCORPORATION_NUMBER: 'businessRegistrationIncorporationNumber',
- FORMATION_INCORPORATION_STATE: 'formationIncorporationState',
FORMATION_INCORPORATION_COUNTRY_CODE: 'formationIncorporationCountryCode',
+ FORMATION_INCORPORATION_STATE: 'formationIncorporationState',
+ BUSINESS_REGISTRATION_INCORPORATION_NUMBER: 'businessRegistrationIncorporationNumber',
+ COUNTRY_CODE: 'countryCode',
+ TAX_ID_EIN_NUMBER: 'taxIDEINNumber',
BUSINESS_CATEGORY: 'natureOfBusiness',
APPLICANT_TYPE_ID: 'applicantTypeID',
+ PURPOSE_OF_TRANSACTION_ID: 'purposeOfTransactionID',
+ CURRENCY_NEEDED: 'currencyNeeded',
+ TRADE_VOLUME: 'tradeVolume',
ANNUAL_VOLUME: 'annualVolume',
+ FUND_DESTINATION_COUNTRIES: 'fundDestinationCountries',
+ FUND_SOURCE_COUNTRIES: 'fundSourceCountries',
+ COMPANY_DIRECTORS_FULL_NAME: 'companyDirectorsFullName',
+ COMPANY_DIRECTORS_JOB_TITLE: 'companyDirectorsJobTitle',
+ COMPANY_DIRECTORS_OCCUPATION: 'companyDirectorsOccupation',
+ OWNS_MORE_THAN_25_PERCENT: 'ownsMoreThan25Percent',
+ ANY_INDIVIDUAL_OWN_25_PERCENT_OR_MORE: 'anyIndividualOwn25PercentOrMore',
+ BENEFICIAL_OWNERS: 'beneficialOwners',
+ ENTITY_CHART: 'entityChart',
+ SIGNER_FULL_NAME: 'signerFullName',
+ SIGNER_DATE_OF_BIRTH: 'signerDateOfBirth',
+ SIGNER_JOB_TITLE: 'signerJobTitle',
+ SIGNER_EMAIL: 'signerEmail',
+ SIGNER_COMPLETE_RESIDENTIAL_ADDRESS: 'signerCompleteResidentialAddress',
+ SECOND_SIGNER_FULL_NAME: 'secondSignerFullName',
+ SECOND_SIGNER_DATE_OF_BIRTH: 'secondSignerDateOfBirth',
+ SECOND_SIGNER_JOB_TITLE: 'secondSignerJobTitle',
+ SECOND_SIGNER_EMAIL: 'secondSignerEmail',
+ SECOND_SIGNER_COMPLETE_RESIDENTIAL_ADDRESS: 'secondSignerCompleteResidentialAddress',
+ SIGNER_PROOF_OF_DIRECTOR: 'signerProofOfDirector',
+ SIGNER_COPY_OF_ID: 'signerCopyOfID',
+ SIGNER_ADDRESS_PROOF: 'signerAddressProof',
+ SIGNER_TAX_ID: 'signerTaxID',
+ SIGNER_PDS_AND_FSG: 'signerPDSAndFSG',
+ SECOND_SIGNER_PROOF_OF_DIRECTOR: 'secondSignerProofOfDirector',
+ SECOND_SIGNER_COPY_OF_ID: 'secondSignerCopyOfID',
+ SECOND_SIGNER_ADDRESS_PROOF: 'secondSignerAddressProof',
+ SECOND_SIGNER_TAX_ID: 'secondSignerTaxID',
+ SECOND_SIGNER_PDS_AND_FSG: 'secondSignerPDSAndFSG',
+ PROVIDE_TRUTHFUL_INFORMATION: 'provideTruthfulInformation',
+ AGREE_TO_TERMS_AND_CONDITIONS: 'agreeToTermsAndConditions',
+ CONSENT_TO_PRIVACY_NOTICE: 'consentToPrivacyNotice',
+ AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT: 'authorizedToBindClientToAgreement',
},
},
} as const;
@@ -151,6 +199,18 @@ type NonUSDReimbursementAccountAdditionalProps = {
/** Country of the account holder */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.ACCOUNT_HOLDER_COUNTRY]: Country | '';
+ /** SWIFT code */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SWIFT_CODE]: string;
+
+ /** Bank name */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.BANK_NAME]: string;
+
+ /** Bank city */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.BANK_CITY]: string;
+
+ /** Bank address line 1 */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.BANK_ADDRESS_LINE_1]: string;
+
/** Bank statement file */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.BANK_STATEMENT]: FileObject[];
@@ -172,34 +232,146 @@ type NonUSDReimbursementAccountAdditionalProps = {
/** Company country */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.COMPANY_COUNTRY]: Country | '';
- /** Company contact number */
+ /** Business contact number */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.BUSINESS_CONTACT_NUMBER]: string;
- /** Company email */
+ /** Business confirmation email */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.BUSINESS_CONFIRMATION_EMAIL]: string;
- /** Company registration number */
- [INPUT_IDS.ADDITIONAL_DATA.CORPAY.BUSINESS_REGISTRATION_INCORPORATION_NUMBER]: string;
-
- /** Company incorporation country */
+ /** Formation incorporation country code */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.FORMATION_INCORPORATION_COUNTRY_CODE]: string;
- /** Company incorporation state */
+ /** Formation incorporation state */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.FORMATION_INCORPORATION_STATE]: string;
- /** Company business category */
+ /** Business registration incorporation number */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.BUSINESS_REGISTRATION_INCORPORATION_NUMBER]: string;
+
+ /** Country code */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.COUNTRY_CODE]: string;
+
+ /** Tax ID EIN number */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.TAX_ID_EIN_NUMBER]: string;
+
+ /** Business category */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.BUSINESS_CATEGORY]: string;
- /** Company applicant type ID */
+ /** Applicant type ID */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.APPLICANT_TYPE_ID]: string;
- /** Company annual volume */
+ /** Purpose of transaction ID */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.PURPOSE_OF_TRANSACTION_ID]: string;
+
+ /** Currency needed */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.CURRENCY_NEEDED]: string;
+
+ /** Trade volume */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.TRADE_VOLUME]: string;
+
+ /** Annual volume */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.ANNUAL_VOLUME]: string;
+
+ /** Fund destination countries */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.FUND_DESTINATION_COUNTRIES]: string;
+
+ /** Fund source countries */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.FUND_SOURCE_COUNTRIES]: string;
+
+ /** Company directors full name */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.COMPANY_DIRECTORS_FULL_NAME]: string;
+
+ /** Company directors job title */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.COMPANY_DIRECTORS_JOB_TITLE]: string;
+
+ /** Company directors occupation */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.COMPANY_DIRECTORS_OCCUPATION]: string;
+
+ /** Owns more than 25 percent */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.ANY_INDIVIDUAL_OWN_25_PERCENT_OR_MORE]: boolean;
+
+ /** Beneficial owners */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.BENEFICIAL_OWNERS]: string;
+
+ /** Entity chart */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.ENTITY_CHART]: FileObject[];
+
+ /** Signer full name */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_FULL_NAME]: string;
+
+ /** Signer date of birth */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_DATE_OF_BIRTH]: string;
+
+ /** Signer job title */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_JOB_TITLE]: string;
+
+ /** Signer email */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_EMAIL]: string;
+
+ /** Signer complete residential address */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_COMPLETE_RESIDENTIAL_ADDRESS]: string;
+
+ /** Second signer full name */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_FULL_NAME]: string;
+
+ /** Second signer date of birth */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_DATE_OF_BIRTH]: string;
+
+ /** Second signer job title */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_JOB_TITLE]: string;
+
+ /** Second signer email */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_EMAIL]: string;
+
+ /** Second signer complete residential address */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_COMPLETE_RESIDENTIAL_ADDRESS]: string;
+
+ /** Signer proof of director */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_PROOF_OF_DIRECTOR]: FileObject[];
+
+ /** Signer copy of ID */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_COPY_OF_ID]: FileObject[];
+
+ /** Signer address proof */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_ADDRESS_PROOF]: FileObject[];
+
+ /** Signer tax ID */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_TAX_ID]: string;
+
+ /** Signer PDS and FSG */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_PDS_AND_FSG]: string;
+
+ /** Second signer proof of director */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_PROOF_OF_DIRECTOR]: FileObject[];
+
+ /** Second signer copy of ID */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_COPY_OF_ID]: FileObject[];
+
+ /** Second signer address proof */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_ADDRESS_PROOF]: FileObject[];
+
+ /** Second signer tax ID */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_TAX_ID]: string;
+
+ /** Second signer PDS and FSG */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_PDS_AND_FSG]: string;
+
+ /** Provide truthful information */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.PROVIDE_TRUTHFUL_INFORMATION]: boolean;
+
+ /** Agree to terms and conditions */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.AGREE_TO_TERMS_AND_CONDITIONS]: boolean;
+
+ /** Consent to privacy notice */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.CONSENT_TO_PRIVACY_NOTICE]: boolean;
+
+ /** Authorized to bind client to agreement */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT]: boolean;
};
type ReimbursementAccountForm = ReimbursementAccountFormExtraProps &
Form<
InputID,
+ // @ts-expect-error TODO: fix it - I have no idea why it is complaining here
BeneficialOwnersStepBaseProps &
BankAccountStepProps &
CompanyStepProps &
diff --git a/src/types/onyx/ReimbursementAccount.ts b/src/types/onyx/ReimbursementAccount.ts
index 17cbe4ac408b..877f9aea65d3 100644
--- a/src/types/onyx/ReimbursementAccount.ts
+++ b/src/types/onyx/ReimbursementAccount.ts
@@ -13,60 +13,141 @@ type BankAccountStep = ValueOf;
/** Substeps to setup a reimbursement bank account */
type BankAccountSubStep = ValueOf;
-/** Modal of Corpay data */
+/** Model of Corpay data */
type Corpay = {
+ /** Swift code */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SWIFT_CODE]?: string;
+ /** Bank name */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.BANK_NAME]: string;
+ /** Bank address - city */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.BANK_CITY]: string;
+ /** Bank address - street and zip code */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.BANK_ADDRESS_LINE_1]: string;
/** Company name */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.COMPANY_NAME]: string;
-
- /** Company street */
+ /** Company address - street */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.COMPANY_STREET]: string;
-
- /** Company city */
- [INPUT_IDS.ADDITIONAL_DATA.CORPAY.COMPANY_CITY]: string;
-
- /** Company state */
+ /** Company address - city */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.COMPANY_CITY]?: string;
+ /** Company address - state (US and CA only) */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.COMPANY_STATE]: string;
-
- /** Company zip code */
+ /** Company address - zip code */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.COMPANY_ZIP_CODE]: string;
-
- /** Company country */
+ /** Company address - zip code */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.COMPANY_COUNTRY]: Country | '';
-
- /** Company contact number */
+ /** Company phone number */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.BUSINESS_CONTACT_NUMBER]: string;
-
- /** Company email */
+ /** Company email address */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.BUSINESS_CONFIRMATION_EMAIL]: string;
-
- /** Company registration number */
- [INPUT_IDS.ADDITIONAL_DATA.CORPAY.BUSINESS_REGISTRATION_INCORPORATION_NUMBER]: string;
-
- /** Company incorporation country */
+ /** Company country */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.FORMATION_INCORPORATION_COUNTRY_CODE]: string;
-
- /** Company incorporation state */
+ /** Company state (US and CA only) */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.FORMATION_INCORPORATION_STATE]: string;
-
- /** Company business category */
- [INPUT_IDS.ADDITIONAL_DATA.CORPAY.BUSINESS_CATEGORY]: string;
-
- /** Company applicant type ID */
+ /** Company registration number */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.BUSINESS_REGISTRATION_INCORPORATION_NUMBER]: string;
+ /** Company country code */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.COUNTRY_CODE]: string;
+ /** Company tax ID number */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.TAX_ID_EIN_NUMBER]: string;
+ /** Incorporation type */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.APPLICANT_TYPE_ID]: string;
-
- /** Company annual volume */
+ /** Nature of business */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.BUSINESS_CATEGORY]: string;
+ /** */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.PURPOSE_OF_TRANSACTION_ID]: string;
+ /** Currency */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.CURRENCY_NEEDED]: string;
+ /** */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.TRADE_VOLUME]: string;
+ /** Annual volume */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.ANNUAL_VOLUME]: string;
-
+ /** */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.FUND_DESTINATION_COUNTRIES]: string;
+ /** */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.FUND_SOURCE_COUNTRIES]: string;
+ /** Director full name */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.COMPANY_DIRECTORS_FULL_NAME]: string;
+ /** Director job title */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.COMPANY_DIRECTORS_JOB_TITLE]: string;
+ /** Director occupation */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.COMPANY_DIRECTORS_OCCUPATION]: string;
+ /** Is user also an owner */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.OWNS_MORE_THAN_25_PERCENT]: boolean;
+ /** Are the more owners */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.ANY_INDIVIDUAL_OWN_25_PERCENT_OR_MORE]: boolean;
+ /** Stringified array of owners data */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.BENEFICIAL_OWNERS]?: string;
+ /** Entity chart files */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.ENTITY_CHART]?: FileObject[];
+ /** Signer full name */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_FULL_NAME]: string;
+ /** Signer DOB */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_DATE_OF_BIRTH]: string;
+ /** Signer job title */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_JOB_TITLE]: string;
+ /** Signer email address */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_EMAIL]: string;
+ /** Signer full address */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_COMPLETE_RESIDENTIAL_ADDRESS]: string;
+ /** Second signer full name */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_FULL_NAME]?: string;
+ /** Second signer DOB */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_DATE_OF_BIRTH]?: string;
+ /** Second signer job title */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_JOB_TITLE]?: string;
+ /** Second signer email address */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_EMAIL]?: string;
+ /** Second signer full address */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_COMPLETE_RESIDENTIAL_ADDRESS]: string;
+ /** URL to uploaded proof of signer being a director */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_PROOF_OF_DIRECTOR]: FileObject[];
+ /** URL to uploaded copy of signer ID */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_COPY_OF_ID]: FileObject[];
+ /** URL to uploaded proof of signer address */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_ADDRESS_PROOF]: FileObject[];
+ /** Signer tax ID */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_TAX_ID]: string;
+ /** Signer PDS and FSG */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SIGNER_PDS_AND_FSG]: string;
+ /** URL to uploaded proof of second signer being a director */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_PROOF_OF_DIRECTOR]?: FileObject[];
+ /** URL to uploaded copy of second signer ID */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_COPY_OF_ID]?: FileObject[];
+ /** URL to uploaded proof of second signer address */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_ADDRESS_PROOF]?: FileObject[];
+ /** Second signer tax ID */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_TAX_ID]?: string;
+ /** Second signer PDS and FSG */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.SECOND_SIGNER_PDS_AND_FSG]?: string;
+ /** Checkbox - provided truthful information */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.PROVIDE_TRUTHFUL_INFORMATION]: boolean;
+ /** Checkbox - agrees to terms and conditions */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.AGREE_TO_TERMS_AND_CONDITIONS]: boolean;
+ /** Checkbox - consents to privacy notice */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.CONSENT_TO_PRIVACY_NOTICE]: boolean;
+ /** Checkbox - authorized to bind to client to agreement */
+ [INPUT_IDS.ADDITIONAL_DATA.CORPAY.AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT]: boolean;
/** Bank statement */
[INPUT_IDS.ADDITIONAL_DATA.CORPAY.BANK_STATEMENT]: FileObject[];
};
-/** Additional data where details of the non-USD reimbursements account are stored */
+/** Model of Additional data */
type AdditionalData = {
- /** Country of the reimbursement account */
+ /** Account holder name */
+ [INPUT_IDS.ADDITIONAL_DATA.ACCOUNT_HOLDER_NAME]: string;
+ /** Account holder address - street */
+ [INPUT_IDS.ADDITIONAL_DATA.ADDRESS_STREET]: string;
+ /** Account holder address - city */
+ [INPUT_IDS.ADDITIONAL_DATA.ADDRESS_CITY]: string;
+ /** Account holder address - state (US and CA only) */
+ [INPUT_IDS.ADDITIONAL_DATA.ADDRESS_STATE]?: string;
+ /** Account holder address - zip code */
+ [INPUT_IDS.ADDITIONAL_DATA.ADDRESS_ZIP_CODE]: string;
+ /** Account holder address - country */
+ [INPUT_IDS.ADDITIONAL_DATA.ACCOUNT_HOLDER_COUNTRY]: string;
+ /** Country user selects in first step */
[INPUT_IDS.ADDITIONAL_DATA.COUNTRY]: Country | '';
-
- /** Details required by Corpay */
+ /** Corpay fields */
corpay: Corpay;
};
diff --git a/src/types/utils/whitelistedReportKeys.ts b/src/types/utils/whitelistedReportKeys.ts
new file mode 100644
index 000000000000..3c566c987526
--- /dev/null
+++ b/src/types/utils/whitelistedReportKeys.ts
@@ -0,0 +1,88 @@
+import type {PolicyReportField, Report} from '@src/types/onyx';
+import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
+
+// List of keys that are allowed on the Report type. These should be the keys that are returned from the server in OpenApp.
+// Before changing this, you need confirmation from an internal engineer that the new key has been added to the report object that is returned from the back-end in OpenApp
+// Any report data that you want to store in Onyx, but isn't returned from the server, should be stored in reportMetaData.
+type WhitelistedReport = OnyxCommon.OnyxValueWithOfflineFeedback<
+ {
+ avatarUrl: unknown;
+ avatarFileName: unknown;
+ chatType: unknown;
+ hasOutstandingChildRequest: unknown;
+ hasOutstandingChildTask: unknown;
+ isOwnPolicyExpenseChat: unknown;
+ isPolicyExpenseChat: unknown;
+ isPinned: unknown;
+ lastMessageText: unknown;
+ lastVisibleActionCreated: unknown;
+ lastReadTime: unknown;
+ lastReadSequenceNumber: unknown;
+ lastMentionedTime: unknown;
+ policyAvatar: unknown;
+ policyName: unknown;
+ oldPolicyName: unknown;
+ hasParentAccess: unknown;
+ description: unknown;
+ isDeletedParentAction: unknown;
+ policyID: unknown;
+ reportName: unknown;
+ reportID: string;
+ reportActionID: unknown;
+ chatReportID: unknown;
+ stateNum: unknown;
+ statusNum: unknown;
+ writeCapability: unknown;
+ type: unknown;
+ visibility: unknown;
+ cachedTotal: unknown;
+ invoiceReceiver: unknown;
+ lastMessageTranslationKey: unknown;
+ parentReportID: unknown;
+ parentReportActionID: unknown;
+ isOptimisticReport: unknown;
+ managerID: unknown;
+ lastVisibleActionLastModified: unknown;
+ displayName: unknown;
+ lastMessageHtml: unknown;
+ lastActorAccountID: unknown;
+ lastActionType: unknown;
+ ownerAccountID: unknown;
+ participants: unknown;
+ total: unknown;
+ unheldTotal: unknown;
+ currency: unknown;
+ errors: unknown;
+ errorFields: unknown;
+ isWaitingOnBankAccount: unknown;
+ isCancelledIOU: unknown;
+ iouReportID: unknown;
+ preexistingReportID: unknown;
+ nonReimbursableTotal: unknown;
+ isHidden: unknown;
+ privateNotes: unknown;
+ isLoadingPrivateNotes: unknown;
+ pendingChatMembers: unknown;
+ transactionThreadReportID: unknown;
+ fieldList: unknown;
+ permissions: unknown;
+ tripData: {
+ startDate: unknown;
+ endDate: unknown;
+ tripID: unknown;
+ };
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ private_isArchived: unknown;
+ },
+ PolicyReportField['fieldID']
+>;
+type ReportKeys = keyof Report;
+type WhitelistedReportKeys = keyof WhitelistedReport;
+
+type ValidateKeys = Exclude extends never ? true : false;
+
+// TypeScript type-level check intended to ensure that all keys in the Report type are part of the whitelisted keys.
+// However, TypeScript doesn't execute code at runtime, so this check is purely for compile-time validation.
+// This validation must be always TRUE.
+const testReportKeys: ValidateKeys = true;
+export default testReportKeys;
diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts
index 81101b74f4d7..2af8a0777c74 100644
--- a/tests/unit/ReportUtilsTest.ts
+++ b/tests/unit/ReportUtilsTest.ts
@@ -975,6 +975,28 @@ describe('ReportUtils', () => {
});
});
+ describe('getQuickActionDetails', () => {
+ it('if the report is archived, the quick action will hide the subtitle and avatar', () => {
+ // Create a fake archived report as quick action report
+ const archivedReport: Report = {
+ ...LHNTestUtils.getFakeReport(),
+ reportID: '1',
+ private_isArchived: DateUtils.getDBTime(),
+ };
+ const reportNameValuePairs = {
+ type: 'chat',
+ private_isArchived: true,
+ };
+
+ // Get the quick action detail
+ const quickActionDetails = ReportUtils.getQuickActionDetails(archivedReport, undefined, undefined, reportNameValuePairs);
+
+ // Expect the quickActionAvatars is empty array and hideQABSubtitle is true since the quick action report is archived
+ expect(quickActionDetails.quickActionAvatars.length).toEqual(0);
+ expect(quickActionDetails.hideQABSubtitle).toEqual(true);
+ });
+ });
+
describe('getChatByParticipants', () => {
const userAccountID = 1;
const userAccountID2 = 2;
diff --git a/tests/unit/validateReportKeysTest.ts b/tests/unit/validateReportKeysTest.ts
new file mode 100644
index 000000000000..1339b126e41b
--- /dev/null
+++ b/tests/unit/validateReportKeysTest.ts
@@ -0,0 +1,8 @@
+import testReportKeys from '@src/types/utils/whitelistedReportKeys';
+
+// This test is mainly to avoid that the testReportKeys is not removed or changed to false
+describe('whitelistedReportKeys', () => {
+ it('testReportKeys must be true', () => {
+ expect(testReportKeys).toBe(true);
+ });
+});