Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10881,6 +10881,8 @@ const CONST = {
PRIVATE_VACATION_DELEGATE_PREFIX: 'private_vacationDelegate_',
/** Onyx prefix for lock account IDs */
PRIVATE_LOCKED_ACCOUNT_PREFIX: 'private_lockAccountDetails_',
/** Onyx prefix for account IDs with a pending unlock request */
PRIVATE_UNLOCK_REQUESTED_PREFIX: 'private_unlockRequested_',

MEMBERS: {
SECONDARY_ACTIONS: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2560,6 +2560,7 @@ const translations: TranslationDeepObject<typeof en> = {
lockAccount: 'Konto sperren',
lockMyAccount: 'Sperren Sie mein Konto',
unlockAccount: 'Konto entsperren',
pendingUnlockRequest: 'Ausstehende Entsperranfrage',
findYourSituation: 'Die meisten Probleme erfordern keine Sperrung Ihres Kontos! Finden Sie Ihre Situation unten:',
lostCardOrCharges:
'<a href="https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-Transaction">Karte verloren oder unbekannte Abbuchungen</a>: Sperren Sie Ihre Karte und kontaktieren Sie Concierge, um unbekannte Transaktionen anzufechten.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2614,6 +2614,7 @@ const translations: TranslationDeepObject<typeof en> = {
lockAccount: 'Κλείδωμα λογαριασμού',
lockMyAccount: 'Κλείδωμα του λογαριασμού μου',
unlockAccount: 'Ξεκλειδώστε τον λογαριασμό',
pendingUnlockRequest: 'Εκκρεμές αίτημα ξεκλειδώματος',
unlockTitle: 'Λάβαμε το αίτημά σας',
unlockDescription: 'Θα εξετάσουμε τον λογαριασμό για να επιβεβαιώσουμε ότι είναι ασφαλές να ξεκλειδωθεί και θα επικοινωνήσουμε μέσω Concierge για τυχόν ερωτήσεις.',
findYourSituation: 'Τα περισσότερα προβλήματα δεν απαιτούν κλείδωμα του λογαριασμού σας. Βρείτε την περίπτωσή σας παρακάτω:',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2718,6 +2718,7 @@ const translations = {
lockAccount: 'Lock account',
lockMyAccount: 'Lock my account',
unlockAccount: 'Unlock account',
pendingUnlockRequest: 'Pending unlock request',
unlockTitle: 'We’ve received your request',
unlockDescription: 'We’ll review the account to verify it’s safe to unlock and reach out via Concierge with any questions.',
findYourSituation: 'Most issues don’t require locking your account! Find your situation below:',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2465,6 +2465,7 @@ const translations: TranslationDeepObject<typeof en> = {
lockAccount: 'Bloquear cuenta',
lockMyAccount: 'Bloquear mi cuenta',
unlockAccount: 'Desbloquear cuenta',
pendingUnlockRequest: 'Solicitud de desbloqueo pendiente',
unlockTitle: 'Hemos recibido tu solicitud',
unlockDescription: 'Revisaremos la cuenta para verificar que sea seguro desbloquearla y nos comunicaremos a través de Concierge si tenemos alguna pregunta.',
findYourSituation: '¡La mayoría de los problemas no requieren bloquear tu cuenta! Busca tu situación a continuación:',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2565,6 +2565,7 @@ const translations: TranslationDeepObject<typeof en> = {
lockAccount: 'Verrouiller le compte',
lockMyAccount: 'Verrouiller mon compte',
unlockAccount: 'Déverrouiller le compte',
pendingUnlockRequest: 'Demande de déverrouillage en attente',
findYourSituation: 'La plupart des problèmes ne nécessitent pas de verrouiller votre compte ! Trouvez votre situation ci-dessous :',
lostCardOrCharges:
'<a href="https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-Transaction">Carte perdue ou frais inconnus</a> : Annulez votre carte et contactez Concierge pour contester les transactions inconnues.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2550,6 +2550,7 @@ const translations: TranslationDeepObject<typeof en> = {
lockAccount: 'Blocca account',
lockMyAccount: 'Blocca il mio account',
unlockAccount: 'Sblocca conto',
pendingUnlockRequest: 'Richiesta di sblocco in sospeso',
findYourSituation: 'La maggior parte dei problemi non richiede il blocco del tuo account! Trova la tua situazione qui sotto:',
lostCardOrCharges:
'<a href="https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-Transaction">Carta smarrita o addebiti non riconosciuti</a>: Annulla la tua carta e contatta Concierge per contestare le transazioni sconosciute.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2534,6 +2534,7 @@ const translations: TranslationDeepObject<typeof en> = {
lockAccount: 'アカウントをロック',
lockMyAccount: 'アカウントをロックする',
unlockAccount: 'アカウントのロック解除',
pendingUnlockRequest: 'ロック解除リクエスト保留中',
findYourSituation: 'ほとんどの問題ではアカウントをロックする必要はありません。以下からご自身の状況をお探しください。',
lostCardOrCharges:
'<a href="https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-Transaction">カードの紛失または身に覚えのない請求</a>:カードを解約し、不明な取引について異議申し立てをするために Concierge へご連絡ください。',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2552,6 +2552,7 @@ const translations: TranslationDeepObject<typeof en> = {
lockAccount: 'Account vergrendelen',
lockMyAccount: 'Account vergrendelen',
unlockAccount: 'Account ontgrendelen',
pendingUnlockRequest: 'Ontgrendelingsverzoek in behandeling',
findYourSituation: 'Voor de meeste problemen hoef je je account niet te blokkeren. Zoek hieronder jouw situatie:',
lostCardOrCharges:
'<a href="https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-Transaction">Kaart kwijt of onbekende kosten</a>: Annuleer je kaart en neem contact op met Concierge om onbekende transacties te betwisten.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2591,6 +2591,7 @@ const translations: TranslationDeepObject<typeof en> = {
lockAccount: 'Zablokuj konto',
lockMyAccount: 'Zablokuj moje konto',
unlockAccount: 'Odblokuj konto',
pendingUnlockRequest: 'Oczekująca prośba o odblokowanie',
findYourSituation: 'Większość problemów nie wymaga blokowania konta! Znajdź swoją sytuację poniżej:',
lostCardOrCharges:
'<a href="https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-Transaction">Zgubiona karta lub nieznane obciążenia</a>: Anuluj swoją kartę i skontaktuj się z Concierge, żeby zakwestionować nieznane transakcje.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2544,6 +2544,7 @@ const translations: TranslationDeepObject<typeof en> = {
lockAccount: 'Bloquear conta',
lockMyAccount: 'Bloquear minha conta',
unlockAccount: 'Desbloquear conta',
pendingUnlockRequest: 'Solicitação de desbloqueio pendente',
findYourSituation: 'A maioria dos problemas não exige bloquear sua conta! Encontre sua situação abaixo:',
lostCardOrCharges:
'<a href="https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-Transaction">Cartão perdido ou cobranças desconhecidas</a>: Cancele seu cartão e contate o Concierge para contestar transações desconhecidas.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2469,6 +2469,7 @@ const translations: TranslationDeepObject<typeof en> = {
lockAccount: '锁定账户',
lockMyAccount: '锁定我的账户',
unlockAccount: '解锁账户',
pendingUnlockRequest: '解锁请求待处理',
findYourSituation: '大多数问题都不需要锁定你的账户!请在以下列表中找到与你相符的情况:',
lostCardOrCharges:
'<a href="https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-Transaction">卡片遗失或出现不明扣款</a>:请挂失您的卡片,并联系 Concierge 申诉未知交易。',
Expand Down
5 changes: 4 additions & 1 deletion src/libs/DomainUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ function hasDomainMemberDetailsErrors(memberDetailsErrors: DomainMemberErrors):
return (
!isEmptyObject(memberDetailsErrors?.vacationDelegateErrors) ||
!isEmptyObject(memberDetailsErrors?.twoFactorAuthExemptEmailsError) ||
!isEmptyObject(memberDetailsErrors?.changeDomainSecurityGroupErrors)
!isEmptyObject(memberDetailsErrors?.changeDomainSecurityGroupErrors) ||
!isEmptyObject(memberDetailsErrors?.lockAccountErrors) ||
!isEmptyObject(memberDetailsErrors?.unlockAccountErrors)
);
}

Expand All @@ -163,6 +165,7 @@ function getMemberCustomRowProps(accountID: number, domainPendingActions: Domain
const mergedErrors = {
...getLatestError(accountIDErrors?.errors),
...getLatestError(accountIDErrors?.lockAccountErrors),
...getLatestError(accountIDErrors?.unlockAccountErrors),
...getLatestError({...accountIDErrors?.changeDomainSecurityGroupErrors, ...emailErrors?.changeDomainSecurityGroupErrors}),
...getLatestError(emailErrors?.errors),
...getLatestError(accountIDErrors?.vacationDelegateErrors),
Expand Down
8 changes: 7 additions & 1 deletion src/libs/actions/Domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1422,10 +1422,16 @@ function clearDomainMemberError(domainAccountID: number, accountID: number, emai
},
});

// Keep the lock and unlock pending actions: those requests may still be queued, and dropping them would let the user send a duplicate
Onyx.merge(`${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`, {
member: {
[email]: null,
[accountID]: null,
[accountID]: {
pendingAction: null,
vacationDelegate: null,
twoFactorAuthExemptEmails: null,
changeDomainSecurityGroup: null,
},
},
});
}
Expand Down
101 changes: 98 additions & 3 deletions src/libs/actions/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1462,14 +1462,17 @@ function lockAccount(currentUserAccountID: number, accountID: number | undefined
let domainSuccessData: DomainOnyxUpdate[] = [];
if (accountID && domainAccountID) {
const userLockKey = `${CONST.DOMAIN.PRIVATE_LOCKED_ACCOUNT_PREFIX}${accountID}`;
const userUnlockRequestedKey = `${CONST.DOMAIN.PRIVATE_UNLOCK_REQUESTED_PREFIX}${accountID}`;

domainOptimisticData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`,
value: {
[userLockKey]: true,
} as PrefixedRecord<typeof CONST.DOMAIN.PRIVATE_LOCKED_ACCOUNT_PREFIX, boolean>,
// Clear any unlock request left over from a previous lock so the new lock doesn't show as pending unlock
[userUnlockRequestedKey]: false,
} as PrefixedRecord<typeof CONST.DOMAIN.PRIVATE_LOCKED_ACCOUNT_PREFIX | typeof CONST.DOMAIN.PRIVATE_UNLOCK_REQUESTED_PREFIX, boolean>,
},
{
onyxMethod: Onyx.METHOD.MERGE,
Expand Down Expand Up @@ -1599,12 +1602,103 @@ function lockAccount(currentUserAccountID: number, accountID: number | undefined
return API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.LOCK_ACCOUNT, params, {optimisticData, successData, failureData});
}

function requestUnlockAccount(accountID: number) {
function requestUnlockAccount(accountID: number, domainAccountID?: number, domainName?: string) {
let optimisticData: DomainOnyxUpdate[] = [];
let successData: DomainOnyxUpdate[] = [];
let failureData: DomainOnyxUpdate[] = [];
if (domainAccountID) {
const userUnlockRequestedKey = `${CONST.DOMAIN.PRIVATE_UNLOCK_REQUESTED_PREFIX}${accountID}`;

// Only mark the request as pending while it's queued; the unlock-requested flag is set once the server receives it
optimisticData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`,
value: {
member: {
[accountID]: {
unlockAccount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
},
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`,
value: {
memberErrors: {
[accountID]: {
unlockAccountErrors: null,
},
},
},
},
];

successData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`,
value: {
[userUnlockRequestedKey]: true,
} as PrefixedRecord<typeof CONST.DOMAIN.PRIVATE_UNLOCK_REQUESTED_PREFIX, boolean>,
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`,
value: {
member: {
[accountID]: {
unlockAccount: null,
},
},
},
},
];

failureData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`,
value: {
member: {
[accountID]: {
unlockAccount: null,
},
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`,
value: {
memberErrors: {
[accountID]: {
unlockAccountErrors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
},
},
},
},
];
}

const params: LockAccountParams = {
accountID,
domainAccountID,
domainName,
};

API.write(WRITE_COMMANDS.REQUEST_UNLOCK_ACCOUNT, params);
API.write(WRITE_COMMANDS.REQUEST_UNLOCK_ACCOUNT, params, {optimisticData, successData, failureData});
}

function clearUnlockAccountError(domainAccountID: number, accountID: number) {
Onyx.merge(`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`, {
memberErrors: {
[accountID]: {
unlockAccountErrors: null,
},
},
});
}

type RespondToProactiveAppReviewParams = {
Expand Down Expand Up @@ -2112,6 +2206,7 @@ export {
clearBetaOverrides,
lockAccount,
requestUnlockAccount,
clearUnlockAccountError,
respondToProactiveAppReview,
verifyAddSecondaryLoginCode,
updateIsVerifiedValidateActionCode,
Expand Down
30 changes: 21 additions & 9 deletions src/pages/domain/Members/DomainMemberDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
setTwoFactorAuthExemptEmailForDomain,
} from '@libs/actions/Domain';
import {clearDraftValues} from '@libs/actions/FormActions';
import {requestUnlockAccount} from '@libs/actions/User';
import {clearUnlockAccountError, requestUnlockAccount} from '@libs/actions/User';
import {getLatestError} from '@libs/ErrorUtils';

import Navigation from '@navigation/Navigation';
Expand All @@ -45,7 +45,7 @@ import type {Domain} from '@src/types/onyx';
import type {OnyxEntry} from 'react-native-onyx';

import {requiresTwoFactorAuthSelector} from '@selectors/Account';
import {accountLockSelector, domainMemberSettingsSelector, domainNameSelector, selectSecurityGroupForAccount, vacationDelegateSelector} from '@selectors/Domain';
import {accountLockSelector, domainMemberSettingsSelector, domainNameSelector, selectSecurityGroupForAccount, unlockRequestedSelector, vacationDelegateSelector} from '@selectors/Domain';
import React, {useCallback, useState} from 'react';
import {View} from 'react-native';

Expand All @@ -55,7 +55,7 @@ function DomainMemberDetailsPage({route}: DomainMemberDetailsPageProps) {
const {domainAccountID, accountID} = route.params;
const styles = useThemeStyles();
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['RemoveMembers', 'Flag', 'Unlock', 'CircularArrowBackwards']);
const icons = useMemoizedLazyExpensifyIcons(['RemoveMembers', 'Flag', 'Unlock', 'Hourglass', 'CircularArrowBackwards']);
const [isModalVisible, setIsModalVisible] = useState(false);
const [shouldForceCloseAccount, setShouldForceCloseAccount] = useState<boolean>();
// We need to use isSmallScreenWidth here because the DecisionModal is opening from RHP and ShouldUseNarrowLayout layout will not work in this place
Expand Down Expand Up @@ -88,6 +88,8 @@ function DomainMemberDetailsPage({route}: DomainMemberDetailsPageProps) {
const memberLogin = personalDetails?.login ?? '';

const [isAccountLocked] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {selector: accountLockSelector(accountID)});
const [isUnlockRequested] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {selector: unlockRequestedSelector(accountID)});
const unlockAccountPendingAction = domainPendingActions?.member?.[accountID]?.unlockAccount;
const lockDomainError = getLatestError(domainErrors?.memberErrors?.[accountID]?.lockAccountErrors);
const lockDomainErrorMessage = Object.values(lockDomainError).at(0);

Expand Down Expand Up @@ -136,7 +138,10 @@ function DomainMemberDetailsPage({route}: DomainMemberDetailsPageProps) {
);

const showUnlockAccountModal = () => {
requestUnlockAccount(accountID);
// Don't send another request while one is already queued or has been received
if (!isUnlockRequested && !unlockAccountPendingAction) {
requestUnlockAccount(accountID, domainAccountID, domainName);
}
showConfirmModal({
title: translate('lockAccountPage.unlockTitle'),
prompt: translate('lockAccountPage.unlockDescription'),
Expand Down Expand Up @@ -211,12 +216,19 @@ function DomainMemberDetailsPage({route}: DomainMemberDetailsPageProps) {
)}

{isAccountLocked ? (
<MenuItemAction
<OfflineWithFeedback
key="UnlockAccount"
title={translate('lockAccountPage.unlockAccount')}
icon={icons.Unlock}
onPress={showUnlockAccountModal}
/>
errorRowStyles={styles.mh5}
pendingAction={unlockAccountPendingAction}
errors={getLatestError(domainErrors?.memberErrors?.[accountID]?.unlockAccountErrors)}
onClose={() => clearUnlockAccountError(domainAccountID, accountID)}
>
<MenuItemAction
title={translate(isUnlockRequested ? 'lockAccountPage.pendingUnlockRequest' : 'lockAccountPage.unlockAccount')}
icon={isUnlockRequested ? icons.Hourglass : icons.Unlock}
onPress={showUnlockAccountModal}
/>
</OfflineWithFeedback>
) : (
<MenuItem.Root
key="ReportSuspiciousActivity"
Expand Down
Loading
Loading