, policy: OnyxEntry): boolean {
+ return PolicyUtils.isPolicyAdmin(policy);
+}
+
/**
* Checks if report action has error when smart scanning
*/
@@ -4932,6 +4999,7 @@ export {
buildOptimisticIOUReportAction,
buildOptimisticReportPreview,
buildOptimisticModifiedExpenseReportAction,
+ buildOptimisticCancelPaymentReportAction,
updateReportPreview,
buildOptimisticTaskReportAction,
buildOptimisticAddCommentReportAction,
@@ -5046,6 +5114,8 @@ export {
getAvailableReportFields,
reportFieldsEnabled,
getAllAncestorReportActionIDs,
+ canEditPolicyDescription,
+ getPolicyDescriptionText,
};
export type {
diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts
index 454b85cc3152..504b2ac27965 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -601,3 +601,5 @@ export {
getRecentTransactions,
hasViolation,
};
+
+export type {TransactionChanges};
diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts
index 52c3ecef156c..55a6c81f0417 100644
--- a/src/libs/UserUtils.ts
+++ b/src/libs/UserUtils.ts
@@ -184,7 +184,7 @@ function getAvatarUrl(avatarSource: AvatarSource | undefined, accountID: number)
* Avatars uploaded by users will have a _128 appended so that the asset server returns a small version.
* This removes that part of the URL so the full version of the image can load.
*/
-function getFullSizeAvatar(avatarSource: AvatarSource, accountID: number): AvatarSource {
+function getFullSizeAvatar(avatarSource: AvatarSource | undefined, accountID: number): AvatarSource {
const source = getAvatar(avatarSource, accountID);
if (typeof source !== 'string') {
return source;
diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts
index 7ee7d6c4f048..02ae638a41d3 100644
--- a/src/libs/ValidationUtils.ts
+++ b/src/libs/ValidationUtils.ts
@@ -5,9 +5,10 @@ import isDate from 'lodash/isDate';
import isEmpty from 'lodash/isEmpty';
import isObject from 'lodash/isObject';
import type {OnyxCollection} from 'react-native-onyx';
+import type {FormInputErrors, FormOnyxKeys, FormOnyxValues} from '@components/Form/types';
import CONST from '@src/CONST';
+import type {OnyxFormKey} from '@src/ONYXKEYS';
import type {Report} from '@src/types/onyx';
-import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import * as CardUtils from './CardUtils';
import DateUtils from './DateUtils';
import * as LoginUtils from './LoginUtils';
@@ -74,8 +75,12 @@ function isValidPastDate(date: string | Date): boolean {
/**
* Used to validate a value that is "required".
+ * @param value - field value
*/
-function isRequiredFulfilled(value: string | Date | unknown[] | Record | null): boolean {
+function isRequiredFulfilled(value?: string | boolean | Date): boolean {
+ if (!value) {
+ return false;
+ }
if (typeof value === 'string') {
return !StringUtils.isEmptyString(value);
}
@@ -91,15 +96,20 @@ function isRequiredFulfilled(value: string | Date | unknown[] | Record(values: FormOnyxValues, requiredFields: Array>): FormInputErrors {
+ const errors: FormInputErrors = {};
+
requiredFields.forEach((fieldKey) => {
- if (isRequiredFulfilled(values[fieldKey])) {
+ if (isRequiredFulfilled(values[fieldKey] as keyof FormOnyxValues)) {
return;
}
+
errors[fieldKey] = 'common.error.fieldRequired';
});
+
return errors;
}
diff --git a/src/libs/WorkspacesSettingsUtils.ts b/src/libs/WorkspacesSettingsUtils.ts
index c41393cb75f7..a3a3d85419e2 100644
--- a/src/libs/WorkspacesSettingsUtils.ts
+++ b/src/libs/WorkspacesSettingsUtils.ts
@@ -72,7 +72,9 @@ const getBrickRoadForPolicy = (report: Report): BrickRoad => {
};
function hasGlobalWorkspaceSettingsRBR(policies: OnyxCollection, policyMembers: OnyxCollection) {
- const cleanPolicies = Object.fromEntries(Object.entries(policies ?? {}).filter(([, policy]) => !!policy));
+ // When attempting to open a policy with an invalid policyID, the policy collection is updated to include policy objects with error information.
+ // Only policies displayed on the policy list page should be verified. Otherwise, the user will encounter an RBR unrelated to any policies on the list.
+ const cleanPolicies = Object.fromEntries(Object.entries(policies ?? {}).filter(([, policy]) => policy?.id));
const cleanAllPolicyMembers = Object.fromEntries(Object.entries(policyMembers ?? {}).filter(([, policyMemberValues]) => !!policyMemberValues));
const errorCheckingMethods: CheckingMethod[] = [
@@ -143,9 +145,9 @@ function getWorkspacesBrickRoads(): Record {
// The key in this map is the workspace id
const workspacesBrickRoadsMap: Record = {};
-
Object.values(allPolicies ?? {}).forEach((policy) => {
- if (!policy) {
+ // Only policies which user has access to on the list should be checked. Policies that don't have an ID and contain only information about the errors aren't displayed anywhere.
+ if (!policy?.id) {
return;
}
diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts
index 2c65804cb428..c5f68317bf18 100644
--- a/src/libs/actions/BankAccounts.ts
+++ b/src/libs/actions/BankAccounts.ts
@@ -7,13 +7,9 @@ import type {
ConnectBankAccountWithPlaidParams,
DeletePaymentBankAccountParams,
OpenReimbursementAccountPageParams,
- UpdateCompanyInformationForBankAccountParams,
- UpdatePersonalInformationForBankAccountParams,
ValidateBankAccountWithTransactionsParams,
VerifyIdentityForBankAccountParams,
} from '@libs/API/parameters';
-import type UpdateBeneficialOwnersForBankAccountParams from '@libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams';
-import type {BankAccountCompanyInformation} from '@libs/API/parameters/UpdateCompanyInformationForBankAccountParams';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
@@ -22,9 +18,9 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
+import type {ACHContractStepProps, BeneficialOwnersStepProps, CompanyStepProps, RequestorStepProps} from '@src/types/form/ReimbursementAccountForm';
import type PlaidBankAccount from '@src/types/onyx/PlaidBankAccount';
import type {BankAccountStep, BankAccountSubStep} from '@src/types/onyx/ReimbursementAccount';
-import type {OnfidoData} from '@src/types/onyx/ReimbursementAccountDraft';
import type {OnyxData} from '@src/types/onyx/Request';
import * as ReimbursementAccount from './ReimbursementAccount';
@@ -47,6 +43,20 @@ type ReimbursementAccountStep = BankAccountStep | '';
type ReimbursementAccountSubStep = BankAccountSubStep | '';
+type BusinessAddress = {
+ addressStreet?: string;
+ addressCity?: string;
+ addressState?: string;
+ addressZipCode?: string;
+};
+
+type PersonalAddress = {
+ requestorAddressStreet?: string;
+ requestorAddressCity?: string;
+ requestorAddressState?: string;
+ requestorAddressZipCode?: string;
+};
+
function clearPlaid(): Promise {
Onyx.set(ONYXKEYS.PLAID_LINK_TOKEN, '');
Onyx.set(ONYXKEYS.PLAID_CURRENT_EVENT, null);
@@ -87,6 +97,7 @@ function clearPersonalBankAccount() {
function clearOnfidoToken() {
Onyx.merge(ONYXKEYS.ONFIDO_TOKEN, '');
+ Onyx.merge(ONYXKEYS.ONFIDO_APPLICANT_ID, '');
}
/**
@@ -133,10 +144,14 @@ function getVBBADataForOnyx(currentStep?: BankAccountStep): OnyxData {
};
}
+function addBusinessWebsiteForDraft(websiteUrl: string) {
+ Onyx.merge(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, {website: websiteUrl});
+}
+
/**
* Submit Bank Account step with Plaid data so php can perform some checks.
*/
-function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAccount: PlaidBankAccount) {
+function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAccount: PlaidBankAccount, policyID: string) {
const parameters: ConnectBankAccountWithPlaidParams = {
bankAccountID,
routingNumber: selectedPlaidBankAccount.routingNumber,
@@ -144,6 +159,8 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc
bank: selectedPlaidBankAccount.bankName,
plaidAccountID: selectedPlaidBankAccount.plaidAccountID,
plaidAccessToken: selectedPlaidBankAccount.plaidAccessToken,
+ canUseNewVbbaFlow: true,
+ policyID,
};
API.write(WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID, parameters, getVBBADataForOnyx());
@@ -156,10 +173,10 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc
*/
function addPersonalBankAccount(account: PlaidBankAccount) {
const parameters: AddPersonalBankAccountParams = {
- addressName: account.addressName,
+ addressName: account.addressName ?? '',
routingNumber: account.routingNumber,
accountNumber: account.accountNumber,
- isSavings: account.isSavings,
+ isSavings: account.isSavings ?? false,
setupType: 'plaid',
bank: account.bankName,
plaidAccountID: account.plaidAccountID,
@@ -234,9 +251,19 @@ function deletePaymentBankAccount(bankAccountID: number) {
* Update the user's personal information on the bank account in database.
*
* This action is called by the requestor step in the Verified Bank Account flow
+ * @param bankAccountID - ID for bank account
+ * @param params - User personal data
*/
-function updatePersonalInformationForBankAccount(params: UpdatePersonalInformationForBankAccountParams) {
- API.write(WRITE_COMMANDS.UPDATE_PERSONAL_INFORMATION_FOR_BANK_ACCOUNT, params, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.REQUESTOR));
+function updatePersonalInformationForBankAccount(bankAccountID: number, params: RequestorStepProps) {
+ API.write(
+ WRITE_COMMANDS.UPDATE_PERSONAL_INFORMATION_FOR_BANK_ACCOUNT,
+ {
+ ...params,
+ bankAccountID,
+ canUseNewVbbaFlow: true,
+ },
+ getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.REQUESTOR),
+ );
}
function validateBankAccount(bankAccountID: number, validateCode: string) {
@@ -283,7 +310,14 @@ function clearReimbursementAccount() {
Onyx.set(ONYXKEYS.REIMBURSEMENT_ACCOUNT, null);
}
-function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subStep: ReimbursementAccountSubStep, localCurrentStep: ReimbursementAccountStep) {
+/**
+ * Function to display and fetch data for Reimbursement Account step
+ * @param stepToOpen - current step to open
+ * @param subStep - particular step
+ * @param localCurrentStep - last step on device
+ * @param policyID - policy ID
+ */
+function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subStep: ReimbursementAccountSubStep, localCurrentStep: ReimbursementAccountStep, policyID: string) {
const onyxData: OnyxData = {
optimisticData: [
{
@@ -318,6 +352,8 @@ function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subS
stepToOpen,
subStep,
localCurrentStep,
+ policyID,
+ canUseNewVbbaFlow: true,
};
return API.read(READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE, parameters, onyxData);
@@ -325,30 +361,64 @@ function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subS
/**
* Updates the bank account in the database with the company step data
+ * @param params - Business step form data
*/
-function updateCompanyInformationForBankAccount(bankAccount: BankAccountCompanyInformation, policyID: string) {
- const parameters: UpdateCompanyInformationForBankAccountParams = {...bankAccount, policyID};
+function updateCompanyInformationForBankAccount(bankAccountID: number, params: CompanyStepProps) {
+ API.write(
+ WRITE_COMMANDS.UPDATE_COMPANY_INFORMATION_FOR_BANK_ACCOUNT,
+ {
+ ...params,
+ bankAccountID,
+ canUseNewVbbaFlow: true,
+ },
+ getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.COMPANY),
+ );
+}
- API.write(WRITE_COMMANDS.UPDATE_COMPANY_INFORMATION_FOR_BANK_ACCOUNT, parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.COMPANY));
+/**
+ * Add beneficial owners for the bank account and verify the accuracy of the information provided
+ * @param params - Beneficial Owners step form params
+ */
+function updateBeneficialOwnersForBankAccount(bankAccountID: number, params: BeneficialOwnersStepProps) {
+ API.write(
+ WRITE_COMMANDS.UPDATE_BENEFICIAL_OWNERS_FOR_BANK_ACCOUNT,
+ {
+ ...params,
+ bankAccountID,
+ canUseNewVbbaFlow: true,
+ },
+ getVBBADataForOnyx(),
+ );
}
/**
- * Add beneficial owners for the bank account, accept the ACH terms and conditions and verify the accuracy of the information provided
+ * Accept the ACH terms and conditions and verify the accuracy of the information provided
+ * @param params - Verification step form params
*/
-function updateBeneficialOwnersForBankAccount(params: UpdateBeneficialOwnersForBankAccountParams) {
- API.write(WRITE_COMMANDS.UPDATE_BENEFICIAL_OWNERS_FOR_BANK_ACCOUNT, params, getVBBADataForOnyx());
+function acceptACHContractForBankAccount(bankAccountID: number, params: ACHContractStepProps) {
+ API.write(
+ WRITE_COMMANDS.ACCEPT_ACH_CONTRACT_FOR_BANK_ACCOUNT,
+ {
+ ...params,
+ bankAccountID,
+ canUseNewVbbaFlow: true,
+ },
+ getVBBADataForOnyx(),
+ );
}
/**
* Create the bank account with manually entered data.
- *
+ * @param plaidMask - scheme for Plaid account number
*/
-function connectBankAccountManually(bankAccountID: number, accountNumber?: string, routingNumber?: string, plaidMask?: string) {
+function connectBankAccountManually(bankAccountID: number, accountNumber?: string, routingNumber?: string, plaidMask?: string, policyID?: string) {
const parameters: ConnectBankAccountManuallyParams = {
bankAccountID,
accountNumber,
routingNumber,
plaidMask,
+ canUseNewVbbaFlow: true,
+ policyID,
};
API.write(WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_MANUALLY, parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT));
@@ -357,10 +427,11 @@ function connectBankAccountManually(bankAccountID: number, accountNumber?: strin
/**
* Verify the user's identity via Onfido
*/
-function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: OnfidoData) {
+function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: Record) {
const parameters: VerifyIdentityForBankAccountParams = {
bankAccountID,
onfidoData: JSON.stringify(onfidoData),
+ canUseNewVbbaFlow: true,
};
API.write(WRITE_COMMANDS.VERIFY_IDENTITY_FOR_BANK_ACCOUNT, parameters, getVBBADataForOnyx());
@@ -421,6 +492,8 @@ function setReimbursementAccountLoading(isLoading: boolean) {
}
export {
+ acceptACHContractForBankAccount,
+ addBusinessWebsiteForDraft,
addPersonalBankAccount,
clearOnfidoToken,
clearPersonalBankAccount,
@@ -443,3 +516,5 @@ export {
verifyIdentityForBankAccount,
setReimbursementAccountLoading,
};
+
+export type {BusinessAddress, PersonalAddress};
diff --git a/src/libs/actions/Console.ts b/src/libs/actions/Console.ts
new file mode 100644
index 000000000000..79276d3307ac
--- /dev/null
+++ b/src/libs/actions/Console.ts
@@ -0,0 +1,31 @@
+import Onyx from 'react-native-onyx';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Log} from '@src/types/onyx';
+
+/**
+ * Merge the new log into the existing logs in Onyx
+ * @param log the log to add
+ */
+function addLog(log: Log) {
+ Onyx.merge(ONYXKEYS.LOGS, {
+ [log.time.getTime()]: log,
+ });
+}
+
+/**
+ * Set whether or not to store logs in Onyx
+ * @param store whether or not to store logs
+ */
+function setShouldStoreLogs(store: boolean) {
+ Onyx.set(ONYXKEYS.SHOULD_STORE_LOGS, store);
+}
+
+/**
+ * Disable logging and flush the logs from Onyx
+ */
+function disableLoggingAndFlushLogs() {
+ setShouldStoreLogs(false);
+ Onyx.set(ONYXKEYS.LOGS, null);
+}
+
+export {addLog, setShouldStoreLogs, disableLoggingAndFlushLogs};
diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts
index ed612a757f4b..3a0bdb94d5f5 100644
--- a/src/libs/actions/FormActions.ts
+++ b/src/libs/actions/FormActions.ts
@@ -1,8 +1,7 @@
import Onyx from 'react-native-onyx';
-import type {KeyValueMapping, NullishDeep} from 'react-native-onyx';
-import type {OnyxFormKeyWithoutDraft} from '@components/Form/types';
+import type {NullishDeep} from 'react-native-onyx';
import FormUtils from '@libs/FormUtils';
-import type {OnyxFormKey} from '@src/ONYXKEYS';
+import type {OnyxFormDraftKey, OnyxFormKey, OnyxValue} from '@src/ONYXKEYS';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
function setIsLoading(formID: OnyxFormKey, isLoading: boolean) {
@@ -25,11 +24,11 @@ function clearErrorFields(formID: OnyxFormKey) {
Onyx.merge(formID, {errorFields: null});
}
-function setDraftValues(formID: OnyxFormKeyWithoutDraft, draftValues: NullishDeep) {
+function setDraftValues(formID: OnyxFormKey, draftValues: NullishDeep>) {
Onyx.merge(FormUtils.getDraftKey(formID), draftValues);
}
-function clearDraftValues(formID: OnyxFormKeyWithoutDraft) {
+function clearDraftValues(formID: OnyxFormKey) {
Onyx.set(FormUtils.getDraftKey(formID), null);
}
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 7fca6614f1a1..3f49890d1d0b 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -291,9 +291,8 @@ function setMoneyRequestDescription(transactionID: string, comment: string, isDr
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {comment: {comment: comment.trim()}});
}
-// eslint-disable-next-line @typescript-eslint/naming-convention
-function setMoneyRequestMerchant_temporaryForRefactor(transactionID: string, merchant: string) {
- Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {merchant: merchant.trim()});
+function setMoneyRequestMerchant(transactionID: string, merchant: string, isDraft: boolean) {
+ Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {merchant});
}
function setMoneyRequestPendingFields(transactionID: string, pendingFields: PendingFields) {
@@ -1490,6 +1489,7 @@ function createSplitsAndOnyxData(
comment: string,
currency: string,
merchant: string,
+ created: string,
category: string,
tag: string,
existingSplitChatReportID = '',
@@ -1509,7 +1509,7 @@ function createSplitsAndOnyxData(
currency,
CONST.REPORT.SPLIT_REPORTID,
comment,
- '',
+ created,
'',
'',
merchant || Localize.translateLocal('iou.request'),
@@ -1721,7 +1721,7 @@ function createSplitsAndOnyxData(
currency,
oneOnOneIOUReport.reportID,
comment,
- '',
+ created,
CONST.IOU.TYPE.SPLIT,
splitTransaction.transactionID,
merchant || Localize.translateLocal('iou.request'),
@@ -1851,11 +1851,13 @@ function splitBill(
comment: string,
currency: string,
merchant: string,
+ created: string,
category: string,
tag: string,
existingSplitChatReportID = '',
billable = false,
) {
+ const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created);
const {splitData, splits, onyxData} = createSplitsAndOnyxData(
participants,
currentUserLogin,
@@ -1864,6 +1866,7 @@ function splitBill(
comment,
currency,
merchant,
+ currentCreated,
category,
tag,
existingSplitChatReportID,
@@ -1878,6 +1881,7 @@ function splitBill(
comment,
category,
merchant,
+ created: currentCreated,
tag,
billable,
transactionID: splitData.transactionID,
@@ -1904,11 +1908,13 @@ function splitBillAndOpenReport(
comment: string,
currency: string,
merchant: string,
+ created: string,
category: string,
tag: string,
billable: boolean,
) {
- const {splitData, splits, onyxData} = createSplitsAndOnyxData(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, merchant, category, tag);
+ const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created);
+ const {splitData, splits, onyxData} = createSplitsAndOnyxData(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, merchant, currentCreated, category, tag);
const parameters: SplitBillParams = {
reportID: splitData.chatReportID,
@@ -1916,6 +1922,7 @@ function splitBillAndOpenReport(
splits: JSON.stringify(splits),
currency,
merchant,
+ created: currentCreated,
comment,
category,
tag,
@@ -3621,6 +3628,95 @@ function submitReport(expenseReport: OnyxTypes.Report) {
API.write(WRITE_COMMANDS.SUBMIT_REPORT, parameters, {optimisticData, successData, failureData});
}
+function cancelPayment(expenseReport: OnyxTypes.Report, chatReport: OnyxTypes.Report) {
+ const optimisticReportAction = ReportUtils.buildOptimisticCancelPaymentReportAction(expenseReport.reportID, -(expenseReport.total ?? 0), expenseReport.currency ?? '');
+ const policy = ReportUtils.getPolicy(chatReport.policyID);
+ const isFree = policy && policy.type === CONST.POLICY.TYPE.FREE;
+ const approvalMode = policy.approvalMode ?? CONST.POLICY.APPROVAL_MODE.BASIC;
+ let stateNum: ValueOf = CONST.REPORT.STATE_NUM.SUBMITTED;
+ let statusNum: ValueOf = CONST.REPORT.STATUS_NUM.SUBMITTED;
+ if (!isFree) {
+ stateNum = approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.APPROVED;
+ statusNum = approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL ? CONST.REPORT.STATUS_NUM.CLOSED : CONST.REPORT.STATUS_NUM.APPROVED;
+ }
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
+ value: {
+ [optimisticReportAction.reportActionID]: {
+ ...(optimisticReportAction as OnyxTypes.ReportAction),
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
+ value: {
+ ...expenseReport,
+ lastMessageText: optimisticReportAction.message?.[0].text,
+ lastMessageHtml: optimisticReportAction.message?.[0].html,
+ stateNum,
+ statusNum,
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
+ value: {
+ [optimisticReportAction.reportActionID]: {
+ pendingAction: null,
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
+ value: {
+ [expenseReport.reportActionID ?? '']: {
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'),
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
+ value: {
+ statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED,
+ },
+ },
+ ];
+
+ if (chatReport?.reportID) {
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`,
+ value: {
+ hasOutstandingChildRequest: true,
+ iouReportID: expenseReport.reportID,
+ },
+ });
+ }
+
+ API.write(
+ WRITE_COMMANDS.CANCEL_PAYMENT,
+ {
+ iouReportID: expenseReport.reportID,
+ chatReportID: chatReport.reportID,
+ managerAccountID: expenseReport.managerID ?? 0,
+ reportActionID: optimisticReportAction.reportActionID,
+ },
+ {optimisticData, successData, failureData},
+ );
+}
+
function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.Report, iouReport: OnyxTypes.Report) {
const recipient = {accountID: iouReport.ownerAccountID};
const {params, optimisticData, successData, failureData} = getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentType);
@@ -3731,10 +3827,6 @@ function setMoneyRequestCurrency(currency: string) {
Onyx.merge(ONYXKEYS.IOU, {currency});
}
-function setMoneyRequestMerchant(merchant: string) {
- Onyx.merge(ONYXKEYS.IOU, {merchant: merchant.trim()});
-}
-
function setMoneyRequestCategory(category: string) {
Onyx.merge(ONYXKEYS.IOU, {category});
}
@@ -3873,7 +3965,6 @@ export {
setMoneyRequestCurrency_temporaryForRefactor,
setMoneyRequestDescription,
setMoneyRequestOriginalCurrency_temporaryForRefactor,
- setMoneyRequestMerchant_temporaryForRefactor,
setMoneyRequestParticipants_temporaryForRefactor,
setMoneyRequestPendingFields,
setMoneyRequestReceipt,
@@ -3901,6 +3992,7 @@ export {
detachReceipt,
getIOUReportID,
editMoneyRequest,
+ cancelPayment,
navigateToStartStepIfScanFileCannotBeRead,
savePreferredPaymentMethod,
};
diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts
index 53491b386b8c..5ae37bb85f10 100644
--- a/src/libs/actions/PersonalDetails.ts
+++ b/src/libs/actions/PersonalDetails.ts
@@ -21,7 +21,8 @@ import * as UserUtils from '@libs/UserUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {DateOfBirthForm, PersonalDetails, PersonalDetailsList, PrivatePersonalDetails} from '@src/types/onyx';
+import type {DateOfBirthForm} from '@src/types/form';
+import type {PersonalDetails, PersonalDetailsList, PrivatePersonalDetails} from '@src/types/onyx';
import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails';
import * as Session from './Session';
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index f197a75871ef..5a65e2e69acb 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -21,6 +21,7 @@ import type {
OpenWorkspaceReimburseViewParams,
UpdateWorkspaceAvatarParams,
UpdateWorkspaceCustomUnitAndRateParams,
+ UpdateWorkspaceDescriptionParams,
UpdateWorkspaceGeneralSettingsParams,
} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
@@ -418,6 +419,7 @@ function removeOptimisticAnnounceRoomMembers(policyID: string, accountIDs: numbe
/**
* Remove the passed members from the policy employeeList
+ * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details
*/
function removeMembers(accountIDs: number[], policyID: string) {
// In case user selects only themselves (admin), their email will be filtered out and the members
@@ -635,6 +637,7 @@ function createPolicyExpenseChats(policyID: string, invitedEmailsToAccountIDs: I
/**
* Adds members to the specified workspace/policyID
+ * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details
*/
function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs, welcomeNote: string, policyID: string) {
const membersListKey = `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}` as const;
@@ -920,6 +923,62 @@ function updateGeneralSettings(policyID: string, name: string, currency: string)
});
}
+function updateDescription(policyID: string, description: string, currentDescription: string) {
+ if (description === currentDescription) {
+ return;
+ }
+ const parsedDescription = ReportUtils.getParsedComment(description);
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ description: parsedDescription,
+ pendingFields: {
+ description: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ errorFields: {
+ description: null,
+ },
+ },
+ },
+ ];
+ const finallyData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ pendingFields: {
+ description: null,
+ },
+ },
+ },
+ ];
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ errorFields: {
+ description: ErrorUtils.getMicroSecondOnyxError('workspace.editor.genericFailureMessage'),
+ },
+ },
+ },
+ ];
+
+ const params: UpdateWorkspaceDescriptionParams = {
+ policyID,
+ description: parsedDescription,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_DESCRIPTION, params, {
+ optimisticData,
+ finallyData,
+ failureData,
+ });
+}
+
function clearWorkspaceGeneralSettingsErrors(policyID: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
errorFields: {
@@ -2067,4 +2126,5 @@ export {
buildOptimisticPolicyRecentlyUsedTags,
createDraftInitialWorkspace,
setWorkspaceInviteMessageDraft,
+ updateDescription,
};
diff --git a/src/libs/actions/ReimbursementAccount/index.js b/src/libs/actions/ReimbursementAccount/index.js
index 217cacf921a6..12b5b940a0f2 100644
--- a/src/libs/actions/ReimbursementAccount/index.js
+++ b/src/libs/actions/ReimbursementAccount/index.js
@@ -12,7 +12,7 @@ export {setBankAccountFormValidationErrors, setPersonalBankAccountFormValidation
* - CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL to ask them to enter their accountNumber and routingNumber
* - CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID to ask them to login to their bank via Plaid
*
- * @param {String} subStep
+ * @param {String | null} subStep
* @returns {Promise}
*/
function setBankAccountSubStep(subStep) {
diff --git a/src/libs/actions/ReimbursementAccount/navigation.js b/src/libs/actions/ReimbursementAccount/navigation.js
index 0ea09465d795..6c82561c16ee 100644
--- a/src/libs/actions/ReimbursementAccount/navigation.js
+++ b/src/libs/actions/ReimbursementAccount/navigation.js
@@ -7,10 +7,9 @@ import ROUTES from '@src/ROUTES';
* Navigate to a specific step in the VBA flow
*
* @param {String} stepID
- * @param {Object} newAchData
*/
-function goToWithdrawalAccountSetupStep(stepID, newAchData) {
- Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {...newAchData, currentStep: stepID}});
+function goToWithdrawalAccountSetupStep(stepID) {
+ Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {currentStep: stepID}});
}
/**
diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js
index 3110c059d2fc..962800fb2e55 100644
--- a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js
+++ b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js
@@ -43,6 +43,11 @@ function resetFreePlanBankAccount(bankAccountID, session) {
key: ONYXKEYS.ONFIDO_TOKEN,
value: '',
},
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.ONFIDO_APPLICANT_ID,
+ value: '',
+ },
{
onyxMethod: Onyx.METHOD.SET,
key: ONYXKEYS.PLAID_DATA,
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 3fbe9cf86e15..4a791dd41b31 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -1972,6 +1972,12 @@ function shouldShowReportActionNotification(reportID: string, action: ReportActi
return false;
}
+ // If this is a whisper targeted to someone else, don't show it
+ if (action && ReportActionsUtils.isWhisperActionTargetedToOthers(action)) {
+ Log.info(`${tag} No notification because the action is whispered to someone else`, false);
+ return false;
+ }
+
// Only show notifications for supported types of report actions
if (!ReportActionsUtils.isNotifiableReportAction(action)) {
Log.info(`${tag} No notification because this action type is not supported`, false, {actionName: action?.actionName});
@@ -2367,7 +2373,9 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: Record {
});
}
+function handleExitToNavigation(exitTo: Routes | HybridAppRoute) {
+ InteractionManager.runAfterInteractions(() => {
+ waitForUserSignIn().then(() => {
+ Navigation.waitForProtectedRoutes().then(() => {
+ const url = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : exitTo;
+ Navigation.navigate(url, CONST.NAVIGATION.TYPE.FORCED_UP);
+ });
+ });
+ });
+}
+
+function signInWithValidateCodeAndNavigate(accountID: number, validateCode: string, twoFactorAuthCode = '', exitTo?: Routes | HybridAppRoute) {
+ signInWithValidateCode(accountID, validateCode, twoFactorAuthCode);
+ if (exitTo) {
+ handleExitToNavigation(exitTo);
+ } else {
+ Navigation.navigate(ROUTES.HOME);
+ }
+}
+
/**
* check if the route can be accessed by anonymous user
*
@@ -890,6 +906,7 @@ export {
checkIfActionIsAllowed,
signIn,
signInWithValidateCode,
+ handleExitToNavigation,
signInWithValidateCodeAndNavigate,
initAutoAuthState,
signInWithShortLivedAuthToken,
diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts
index 8f2f01cde3ac..1d9af01f2fa0 100644
--- a/src/libs/actions/Transaction.ts
+++ b/src/libs/actions/Transaction.ts
@@ -106,7 +106,7 @@ function saveWaypoint(transactionID: string, index: string, waypoint: RecentWayp
}
}
-function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: boolean): Promise {
+function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: boolean) {
// Index comes from the route params and is a string
const index = Number(currentIndex);
const existingWaypoints = transaction?.comment?.waypoints ?? {};
@@ -115,7 +115,7 @@ function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft:
const waypointValues = Object.values(existingWaypoints);
const removed = waypointValues.splice(index, 1);
if (removed.length === 0) {
- return Promise.resolve();
+ return;
}
const isRemovedWaypointEmpty = removed.length > 0 && !TransactionUtils.waypointHasValidAddress(removed[0] ?? {});
@@ -164,9 +164,10 @@ function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft:
};
}
if (isDraft) {
- return Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction.transactionID}`, newTransaction);
+ Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction.transactionID}`, newTransaction);
+ return;
}
- return Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, newTransaction);
+ Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, newTransaction);
}
function getOnyxDataForRouteRequest(transactionID: string, isDraft = false): OnyxData {
diff --git a/src/libs/focusEditAfterCancelDelete/index.native.ts b/src/libs/focusEditAfterCancelDelete/index.native.ts
new file mode 100755
index 000000000000..17bafabc5790
--- /dev/null
+++ b/src/libs/focusEditAfterCancelDelete/index.native.ts
@@ -0,0 +1,8 @@
+import {InteractionManager} from 'react-native';
+import type FocusEditAfterCancelDelete from './types';
+
+const focusEditAfterCancelDelete: FocusEditAfterCancelDelete = (textInputRef) => {
+ InteractionManager.runAfterInteractions(() => textInputRef?.focus());
+};
+
+export default focusEditAfterCancelDelete;
diff --git a/src/libs/focusEditAfterCancelDelete/index.ts b/src/libs/focusEditAfterCancelDelete/index.ts
new file mode 100755
index 000000000000..541c0ef1aaef
--- /dev/null
+++ b/src/libs/focusEditAfterCancelDelete/index.ts
@@ -0,0 +1,5 @@
+import type FocusEditAfterCancelDelete from './types';
+
+const focusEditAfterCancelDelete: FocusEditAfterCancelDelete = () => {};
+
+export default focusEditAfterCancelDelete;
diff --git a/src/libs/focusEditAfterCancelDelete/types.ts b/src/libs/focusEditAfterCancelDelete/types.ts
new file mode 100755
index 000000000000..ee479203f890
--- /dev/null
+++ b/src/libs/focusEditAfterCancelDelete/types.ts
@@ -0,0 +1,5 @@
+import type {TextInput} from 'react-native';
+
+type FocusEditAfterCancelDelete = (inputRef: TextInput | HTMLTextAreaElement | null) => void;
+
+export default FocusEditAfterCancelDelete;
diff --git a/src/libs/getCurrentPosition/index.android.ts b/src/libs/getCurrentPosition/index.android.ts
index bd27944b8830..8b0d1c76d25f 100644
--- a/src/libs/getCurrentPosition/index.android.ts
+++ b/src/libs/getCurrentPosition/index.android.ts
@@ -1,7 +1,7 @@
// https://github.com/Richou/react-native-android-location-enabler/issues/40
// If we update our react native version, we need to test this file again
import Geolocation from '@react-native-community/geolocation';
-import RNAndroidLocationEnabler from 'react-native-android-location-enabler';
+import {promptForEnableLocationIfNeeded} from 'react-native-android-location-enabler';
import type {GetCurrentPosition} from './getCurrentPosition.types';
import {GeolocationErrorCode} from './getCurrentPosition.types';
@@ -15,9 +15,8 @@ const getCurrentPosition: GetCurrentPosition = (success, error, config) => {
// Prompt's the user to enable geolocation permission with yes/no options
// If the user selects yes, then this module would enable the native system location
// Otherwise if user selects no, or we have an issue displaying the prompt, it will return an error
- RNAndroidLocationEnabler.promptForEnableLocationIfNeeded({
+ promptForEnableLocationIfNeeded({
interval: 2000, // This updates location after every 2 seconds (required prop). We don't depend on this as we only use the location once.
- fastInterval: 1, // The shortest time (1 ms) our app is willing to wait for location update. Passing 0 ms short's the internal ternary condition of library to default value.
})
.then((permissionState) => {
if (permissionState === 'enabled') {
diff --git a/src/libs/localFileCreate/index.native.ts b/src/libs/localFileCreate/index.native.ts
new file mode 100644
index 000000000000..418701ae3ff5
--- /dev/null
+++ b/src/libs/localFileCreate/index.native.ts
@@ -0,0 +1,19 @@
+import RNFetchBlob from 'react-native-blob-util';
+import * as FileUtils from '@libs/fileDownload/FileUtils';
+import type LocalFileCreate from './types';
+
+/**
+ * Creates a blob file using RN Fetch Blob
+ * @param fileName name of the file
+ * @param textContent content of the file
+ * @returns path, filename and size of the newly created file
+ */
+const localFileCreate: LocalFileCreate = (fileName, textContent) => {
+ const newFileName = FileUtils.appendTimeToFileName(fileName);
+ const dir = RNFetchBlob.fs.dirs.DocumentDir;
+ const path = `${dir}/${newFileName}.txt`;
+
+ return RNFetchBlob.fs.writeFile(path, textContent, 'utf8').then(() => RNFetchBlob.fs.stat(path).then(({size}) => ({path, newFileName, size})));
+};
+
+export default localFileCreate;
diff --git a/src/libs/localFileCreate/index.ts b/src/libs/localFileCreate/index.ts
new file mode 100644
index 000000000000..0178a6c76f7c
--- /dev/null
+++ b/src/libs/localFileCreate/index.ts
@@ -0,0 +1,18 @@
+import * as FileUtils from '@libs/fileDownload/FileUtils';
+import type LocalFileCreate from './types';
+
+/**
+ * Creates a Blob file
+ * @param fileName name of the file
+ * @param textContent content of the file
+ * @returns path, filename and size of the newly created file
+ */
+const localFileCreate: LocalFileCreate = (fileName, textContent) => {
+ const newFileName = FileUtils.appendTimeToFileName(fileName);
+ const blob = new Blob([textContent], {type: 'text/plain'});
+ const url = URL.createObjectURL(blob);
+
+ return Promise.resolve({path: url, newFileName, size: blob.size});
+};
+
+export default localFileCreate;
diff --git a/src/libs/localFileCreate/types.ts b/src/libs/localFileCreate/types.ts
new file mode 100644
index 000000000000..e8e8084cb567
--- /dev/null
+++ b/src/libs/localFileCreate/types.ts
@@ -0,0 +1,3 @@
+type LocalFileCreate = (fileName: string, textContent: string) => Promise<{path: string; newFileName: string; size: number}>;
+
+export default LocalFileCreate;
diff --git a/src/libs/localFileDownload/index.android.ts b/src/libs/localFileDownload/index.android.ts
index dd266d3be405..6573006154d4 100644
--- a/src/libs/localFileDownload/index.android.ts
+++ b/src/libs/localFileDownload/index.android.ts
@@ -1,5 +1,6 @@
import RNFetchBlob from 'react-native-blob-util';
import * as FileUtils from '@libs/fileDownload/FileUtils';
+import localFileCreate from '@libs/localFileCreate';
import type LocalFileDownload from './types';
/**
@@ -8,11 +9,7 @@ import type LocalFileDownload from './types';
* After the file is copied, it is removed from the internal dir.
*/
const localFileDownload: LocalFileDownload = (fileName, textContent, successMessage) => {
- const newFileName = FileUtils.appendTimeToFileName(fileName);
- const dir = RNFetchBlob.fs.dirs.DocumentDir;
- const path = `${dir}/${newFileName}.txt`;
-
- RNFetchBlob.fs.writeFile(path, textContent, 'utf8').then(() => {
+ localFileCreate(fileName, textContent).then(({path, newFileName}) => {
RNFetchBlob.MediaCollection.copyToMediaStore(
{
name: newFileName,
diff --git a/src/libs/localFileDownload/index.ios.ts b/src/libs/localFileDownload/index.ios.ts
index 892ab29d21f5..778d19d9449b 100644
--- a/src/libs/localFileDownload/index.ios.ts
+++ b/src/libs/localFileDownload/index.ios.ts
@@ -1,6 +1,6 @@
import {Share} from 'react-native';
import RNFetchBlob from 'react-native-blob-util';
-import * as FileUtils from '@libs/fileDownload/FileUtils';
+import localFileCreate from '@libs/localFileCreate';
import type LocalFileDownload from './types';
/**
@@ -9,11 +9,7 @@ import type LocalFileDownload from './types';
* After the file is shared, it is removed from the internal dir.
*/
const localFileDownload: LocalFileDownload = (fileName, textContent) => {
- const newFileName = FileUtils.appendTimeToFileName(fileName);
- const dir = RNFetchBlob.fs.dirs.DocumentDir;
- const path = `${dir}/${newFileName}.txt`;
-
- RNFetchBlob.fs.writeFile(path, textContent, 'utf8').then(() => {
+ localFileCreate(fileName, textContent).then(({path, newFileName}) => {
Share.share({url: path, title: newFileName}).finally(() => {
RNFetchBlob.fs.unlink(path);
});
diff --git a/src/libs/localFileDownload/index.ts b/src/libs/localFileDownload/index.ts
index ba038b8853ad..a1a20a0e3d4a 100644
--- a/src/libs/localFileDownload/index.ts
+++ b/src/libs/localFileDownload/index.ts
@@ -1,4 +1,4 @@
-import * as FileUtils from '@libs/fileDownload/FileUtils';
+import localFileCreate from '@libs/localFileCreate';
import type LocalFileDownload from './types';
/**
@@ -7,12 +7,12 @@ import type LocalFileDownload from './types';
* is downloaded by the browser.
*/
const localFileDownload: LocalFileDownload = (fileName, textContent) => {
- const blob = new Blob([textContent], {type: 'text/plain'});
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.download = FileUtils.appendTimeToFileName(`${fileName}.txt`);
- link.href = url;
- link.click();
+ localFileCreate(`${fileName}.txt`, textContent).then(({path, newFileName}) => {
+ const link = document.createElement('a');
+ link.download = newFileName;
+ link.href = path;
+ link.click();
+ });
};
export default localFileDownload;
diff --git a/src/libs/onyxSubscribe.ts b/src/libs/onyxSubscribe.ts
index 3a2daca900e4..afb8818ced40 100644
--- a/src/libs/onyxSubscribe.ts
+++ b/src/libs/onyxSubscribe.ts
@@ -8,7 +8,7 @@ import type {OnyxCollectionKey, OnyxKey} from '@src/ONYXKEYS';
* @param mapping Same as for Onyx.connect()
* @return Unsubscribe callback
*/
-function onyxSubscribe(mapping: ConnectOptions) {
+function onyxSubscribe(mapping: ConnectOptions) {
const connectionId = Onyx.connect(mapping);
return () => Onyx.disconnect(connectionId);
}
diff --git a/src/libs/searchCountryOptions.ts b/src/libs/searchCountryOptions.ts
index 1fc5d343f556..953a5c81c77f 100644
--- a/src/libs/searchCountryOptions.ts
+++ b/src/libs/searchCountryOptions.ts
@@ -22,18 +22,54 @@ function searchCountryOptions(searchValue: string, countriesData: CountryData[])
if (!trimmedSearchValue) {
return [];
}
-
const filteredData = countriesData.filter((country) => country.searchValue.includes(trimmedSearchValue));
- return filteredData.sort((a, b) => {
- if (a.value.toLowerCase() === trimmedSearchValue) {
+ const halfSorted = filteredData.sort((a, b) => {
+ // Prioritize matches at the beginning of the string
+ // e.g. For the search term "Bar" "Barbados" should be prioritized over Antigua & Barbuda
+ // The first two characters are the country code, so we start at index 2
+ // and end at the length of the search term
+ const countryNameASubstring = a.searchValue.toLowerCase().substring(2, trimmedSearchValue.length + 2);
+ const countryNameBSubstring = b.searchValue.toLowerCase().substring(2, trimmedSearchValue.length + 2);
+ if (countryNameASubstring === trimmedSearchValue.toLowerCase()) {
return -1;
}
- if (b.value.toLowerCase() === trimmedSearchValue) {
+ if (countryNameBSubstring === trimmedSearchValue.toLowerCase()) {
return 1;
}
return 0;
});
+
+ let fullSorted;
+ const unsanitizedSearchValue = searchValue.toLowerCase().trim();
+ if (trimmedSearchValue !== unsanitizedSearchValue) {
+ // Diacritic detected, prioritize diacritic matches
+ // We search for diacritic matches by using the unsanitized country name and search term
+ fullSorted = halfSorted.sort((a, b) => {
+ const unsanitizedCountryNameA = a.text.toLowerCase();
+ const unsanitizedCountryNameB = b.text.toLowerCase();
+ if (unsanitizedCountryNameA.includes(unsanitizedSearchValue)) {
+ return -1;
+ }
+ if (unsanitizedCountryNameB.includes(unsanitizedSearchValue)) {
+ return 1;
+ }
+ return 0;
+ });
+ } else {
+ // Diacritic not detected, prioritize country code matches (country codes can never contain diacritics)
+ // E.g. the search term 'US' should push 'United States' to the top
+ fullSorted = halfSorted.sort((a, b) => {
+ if (a.value.toLowerCase() === trimmedSearchValue) {
+ return -1;
+ }
+ if (b.value.toLowerCase() === trimmedSearchValue) {
+ return 1;
+ }
+ return 0;
+ });
+ }
+ return fullSorted;
}
export default searchCountryOptions;
diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.tsx
similarity index 72%
rename from src/pages/DetailsPage.js
rename to src/pages/DetailsPage.tsx
index a4cafd59cb73..d4438d3141bf 100755
--- a/src/pages/DetailsPage.js
+++ b/src/pages/DetailsPage.tsx
@@ -1,10 +1,9 @@
+import type {StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React from 'react';
import {ScrollView, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import type {OnyxEntry} from 'react-native-onyx';
import AttachmentModal from '@components/AttachmentModal';
import AutoUpdateTime from '@components/AutoUpdateTime';
import Avatar from '@components/Avatar';
@@ -18,77 +17,51 @@ import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import UserDetailsTooltip from '@components/UserDetailsTooltip';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import {parsePhoneNumber} from '@libs/PhoneNumber';
import * as ReportUtils from '@libs/ReportUtils';
import * as UserUtils from '@libs/UserUtils';
+import type {DetailsNavigatorParamList} from '@navigation/types';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
-import personalDetailsPropType from './personalDetailsPropType';
-
-const matchType = PropTypes.shape({
- params: PropTypes.shape({
- /** login passed via route /details/:login */
- login: PropTypes.string,
-
- /** report ID passed */
- reportID: PropTypes.string,
- }),
-});
-
-const propTypes = {
- /* Onyx Props */
+import type SCREENS from '@src/SCREENS';
+import type {PersonalDetails, PersonalDetailsList, Session} from '@src/types/onyx';
+type DetailsPageOnyxProps = {
/** The personal details of the person who is logged in */
- personalDetails: personalDetailsPropType,
-
- /** Route params */
- route: matchType.isRequired,
+ personalDetails: OnyxEntry;
/** Session info for the currently logged in user. */
- session: PropTypes.shape({
- /** Currently logged in user accountID */
- accountID: PropTypes.number,
- }),
-
- ...withLocalizePropTypes,
+ session: OnyxEntry;
};
-const defaultProps = {
- // When opening someone else's profile (via deep link) before login, this is empty
- personalDetails: {},
- session: {
- accountID: 0,
- },
-};
+type DetailsPageProps = DetailsPageOnyxProps & StackScreenProps;
/**
* Gets the phone number to display for SMS logins
- *
- * @param {Object} details
- * @param {String} details.login
- * @param {String} details.displayName
- * @returns {String}
*/
-const getPhoneNumber = (details) => {
+const getPhoneNumber = ({login = '', displayName = ''}: PersonalDetails): string | undefined => {
// If the user hasn't set a displayName, it is set to their phone number, so use that
- const parsedPhoneNumber = parsePhoneNumber(details.displayName);
+ const parsedPhoneNumber = parsePhoneNumber(displayName);
if (parsedPhoneNumber.possible) {
- return parsedPhoneNumber.number.e164;
+ return parsedPhoneNumber?.number?.e164;
}
// If the user has set a displayName, get the phone number from the SMS login
- return details.login ? Str.removeSMSDomain(details.login) : '';
+ return login ? Str.removeSMSDomain(login) : '';
};
-function DetailsPage(props) {
+function DetailsPage({personalDetails, route, session}: DetailsPageProps) {
const styles = useThemeStyles();
- const login = lodashGet(props.route.params, 'login', '');
- let details = _.find(props.personalDetails, (detail) => detail.login === login.toLowerCase());
+ const {translate, formatPhoneNumber} = useLocalize();
+ const login = route.params?.login ?? '';
+ const sessionAccountID = session?.accountID ?? 0;
+
+ let details = Object.values(personalDetails ?? {}).find((personalDetail) => personalDetail?.login === login.toLowerCase());
if (!details) {
if (login === CONST.EMAIL.CONCIERGE) {
@@ -116,44 +89,44 @@ function DetailsPage(props) {
if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) {
const localeKey = pronouns.replace(CONST.PRONOUNS.PREFIX, '');
- pronouns = props.translate(`pronouns.${localeKey}`);
+ pronouns = translate(`pronouns.${localeKey}` as TranslationPaths);
}
const phoneNumber = getPhoneNumber(details);
const phoneOrEmail = isSMSLogin ? getPhoneNumber(details) : details.login;
const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(details, '', false);
- const isCurrentUser = props.session.accountID === details.accountID;
+ const isCurrentUser = sessionAccountID === details.accountID;
return (
-
-
+
+
{details ? (
{({show}) => (
-
+
@@ -173,11 +146,11 @@ function DetailsPage(props) {
style={[styles.textLabelSupporting, styles.mb1]}
numberOfLines={1}
>
- {props.translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')}
+ {translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')}
-
+
- {isSMSLogin ? props.formatPhoneNumber(phoneNumber) : details.login}
+ {isSMSLogin ? formatPhoneNumber(phoneNumber ?? '') : details.login}
@@ -188,16 +161,16 @@ function DetailsPage(props) {
style={[styles.textLabelSupporting, styles.mb1]}
numberOfLines={1}
>
- {props.translate('profilePage.preferredPronouns')}
+ {translate('profilePage.preferredPronouns')}
{pronouns}
) : null}
- {shouldShowLocalTime && }
+ {shouldShowLocalTime && }