Skip to content

Commit

Permalink
Merge pull request Expensify#38998 from software-mansion-labs/@szymcz…
Browse files Browse the repository at this point in the history
…ak/update-cards-section

Update the Assigned Cards Section of the Wallet Page
  • Loading branch information
grgia authored Apr 29, 2024
2 parents 30bfabc + f915905 commit 6a37cd8
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 150 deletions.
16 changes: 8 additions & 8 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,12 @@ const ROUTES = {
SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links',
SETTINGS_WALLET: 'settings/wallet',
SETTINGS_WALLET_DOMAINCARD: {
route: 'settings/wallet/card/:domain',
getRoute: (domain: string) => `settings/wallet/card/${domain}` as const,
route: 'settings/wallet/card/:cardID?',
getRoute: (cardID: string) => `settings/wallet/card/${cardID}` as const,
},
SETTINGS_REPORT_FRAUD: {
route: 'settings/wallet/card/:domain/report-virtual-fraud',
getRoute: (domain: string) => `settings/wallet/card/${domain}/report-virtual-fraud` as const,
route: 'settings/wallet/card/:cardID/report-virtual-fraud',
getRoute: (cardID: string) => `settings/wallet/card/${cardID}/report-virtual-fraud` as const,
},
SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME: {
route: 'settings/wallet/card/:domain/get-physical/name',
Expand Down Expand Up @@ -131,12 +131,12 @@ const ROUTES = {
SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance',
SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account',
SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED: {
route: 'settings/wallet/card/:domain/report-card-lost-or-damaged',
getRoute: (domain: string) => `settings/wallet/card/${domain}/report-card-lost-or-damaged` as const,
route: 'settings/wallet/card/:cardID/report-card-lost-or-damaged',
getRoute: (cardID: string) => `settings/wallet/card/${cardID}/report-card-lost-or-damaged` as const,
},
SETTINGS_WALLET_CARD_ACTIVATE: {
route: 'settings/wallet/card/:domain/activate',
getRoute: (domain: string) => `settings/wallet/card/${domain}/activate` as const,
route: 'settings/wallet/card/:cardID/activate',
getRoute: (cardID: string) => `settings/wallet/card/${cardID}/activate` as const,
},
SETTINGS_LEGAL_NAME: 'settings/profile/legal-name',
SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth',
Expand Down
12 changes: 12 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,18 @@ export default {
cardPage: {
expensifyCard: 'Expensify Card',
availableSpend: 'Remaining limit',
smartLimit: {
name: 'Smart limit',
title: (formattedLimit: string) => `You can spend up to ${formattedLimit} on this card, and the limit will reset as your submitted expenses are approved.`,
},
fixedLimit: {
name: 'Fixed limit',
title: (formattedLimit: string) => `You can spend up to ${formattedLimit} on this card, and then it will deactivate.`,
},
monthlyLimit: {
name: 'Monthly limit',
title: (formattedLimit: string) => `You can spend up to ${formattedLimit} on this card per month. The limit will reset on the 1st day of each calendar month.`,
},
virtualCardNumber: 'Virtual card number',
physicalCardNumber: 'Physical card number',
getPhysicalCard: 'Get physical card',
Expand Down
12 changes: 12 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,18 @@ export default {
cardPage: {
expensifyCard: 'Tarjeta Expensify',
availableSpend: 'Límite restante',
smartLimit: {
name: 'Límite inteligente',
title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta al mes. El límite se restablecerá el primer día del mes.`,
},
fixedLimit: {
name: 'Límite fijo',
title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta, luego se desactivará.`,
},
monthlyLimit: {
name: 'Límite mensual',
title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta y el límite se restablecerá a medida que se aprueben tus gastos.`,
},
virtualCardNumber: 'Número de la tarjeta virtual',
physicalCardNumber: 'Número de la tarjeta física',
getPhysicalCard: 'Obtener tarjeta física',
Expand Down
2 changes: 1 addition & 1 deletion src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ function maskCard(lastFour = ''): string {
* @returns a physical card object (or undefined if none is found)
*/
function findPhysicalCard(cards: Card[]) {
return cards.find((card) => !card.nameValuePairs?.isVirtual);
return cards.find((card) => !card?.nameValuePairs?.isVirtual);
}

/**
Expand Down
28 changes: 20 additions & 8 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,27 +118,36 @@ type SettingsNavigatorParamList = {
};
[SCREENS.SETTINGS.WALLET.ROOT]: undefined;
[SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: undefined;
[SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: undefined;
[SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: undefined;
[SCREENS.SETTINGS.WALLET.CARD_ACTIVATE]: undefined;
[SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: {
/** cardID of selected card */
cardID: string;
};
[SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: {
/** cardID of selected card */
cardID: string;
};
[SCREENS.SETTINGS.WALLET.CARD_ACTIVATE]: {
/** cardID of selected card */
cardID: string;
};
[SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.NAME]: {
/** domain passed via route /settings/wallet/card/:domain */
/** domain of selected card */
domain: string;
};
[SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.PHONE]: {
/** domain passed via route /settings/wallet/card/:domain */
/** domain of selected card */
domain: string;
};
[SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.ADDRESS]: {
/** Currently selected country */
country: string;
/** domain passed via route /settings/wallet/card/:domain */
/** domain of selected card */
domain: string;
};
[SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.CONFIRM]: {
/** Currently selected country */
country: string;
/** domain passed via route /settings/wallet/card/:domain */
/** domain of selected card */
domain: string;
};
[SCREENS.WORKSPACE.WORKFLOWS_PAYER]: {
Expand Down Expand Up @@ -302,7 +311,10 @@ type SettingsNavigatorParamList = {
backTo: Routes;
};
[SCREENS.SETTINGS.TWO_FACTOR_AUTH]: BackToParams;
[SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: undefined;
[SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: {
/** cardID of selected card */
cardID: string;
};
[SCREENS.KEYBOARD_SHORTCUTS]: undefined;
[SCREENS.SETTINGS.EXIT_SURVEY.REASON]: undefined;
[SCREENS.SETTINGS.EXIT_SURVEY.RESPONSE]: {
Expand Down
4 changes: 2 additions & 2 deletions src/libs/migrations/RenameCardIsVirtual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function () {
Log.info('[Migrate Onyx] Skipped migration RenameCardIsVirtual because there are no cards linked to the account');
return resolve();
}
const cardsWithIsVirtualProp = Object.values(cardList).filter((card) => card.isVirtual !== undefined);
const cardsWithIsVirtualProp = Object.values(cardList).filter((card) => card?.nameValuePairs?.isVirtual !== undefined);
if (!cardsWithIsVirtualProp.length) {
Log.info('[Migrate Onyx] Skipped migration RenameCardIsVirtual because there were no cards with the isVirtual property');
return resolve();
Expand All @@ -34,7 +34,7 @@ export default function () {
...result,
[card.cardID]: {
nameValuePairs: {
isVirtual: card.isVirtual,
isVirtual: card?.nameValuePairs?.isVirtual,
},
isVirtual: undefined,
},
Expand Down
44 changes: 23 additions & 21 deletions src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ import useNetwork from '@hooks/useNetwork';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as CardUtils from '@libs/CardUtils';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {PublicScreensParamList} from '@libs/Navigation/types';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import * as CardSettings from '@userActions/Card';
import CONST from '@src/CONST';
Expand All @@ -34,15 +33,15 @@ type ActivatePhysicalCardPageOnyxProps = {
cardList: OnyxEntry<Record<string, Card>>;
};

type ActivatePhysicalCardPageProps = ActivatePhysicalCardPageOnyxProps & StackScreenProps<PublicScreensParamList, typeof SCREENS.TRANSITION_BETWEEN_APPS>;
type ActivatePhysicalCardPageProps = ActivatePhysicalCardPageOnyxProps & StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.WALLET.CARD_ACTIVATE>;

const LAST_FOUR_DIGITS_LENGTH = 4;
const MAGIC_INPUT_MIN_HEIGHT = 86;

function ActivatePhysicalCardPage({
cardList,
route: {
params: {domain = ''},
params: {cardID = ''},
},
}: ActivatePhysicalCardPageProps) {
const theme = useTheme();
Expand All @@ -55,30 +54,30 @@ function ActivatePhysicalCardPage({
const [lastFourDigits, setLastFourDigits] = useState('');
const [lastPressedDigit, setLastPressedDigit] = useState('');

const domainCards = CardUtils.getDomainCards(cardList)[domain] ?? [];
const physicalCard = domainCards.find((card) => !card.nameValuePairs?.isVirtual);
const cardID = physicalCard?.cardID ?? 0;
const cardError = ErrorUtils.getLatestErrorMessage(physicalCard ?? {});
const inactiveCard = cardList?.[cardID];
const cardError = ErrorUtils.getLatestErrorMessage(inactiveCard ?? {});

const activateCardCodeInputRef = useRef<MagicCodeInputHandle>(null);

/**
* If state of the card is CONST.EXPENSIFY_CARD.STATE.OPEN, navigate to card details screen.
*/
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (physicalCard?.isLoading || cardList?.[cardID]?.state !== CONST.EXPENSIFY_CARD.STATE.OPEN) {
if (inactiveCard?.state !== CONST.EXPENSIFY_CARD.STATE.OPEN || inactiveCard?.isLoading) {
return;
}

Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain));
}, [cardID, cardList, domain, physicalCard?.isLoading]);
Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID));
}, [cardID, cardList, inactiveCard?.isLoading, inactiveCard?.state]);

useEffect(
() => () => {
CardSettings.clearCardListErrors(cardID);
if (!inactiveCard?.cardID) {
return;
}
CardSettings.clearCardListErrors(inactiveCard?.cardID);
},
[cardID],
[inactiveCard?.cardID],
);

/**
Expand All @@ -95,8 +94,8 @@ function ActivatePhysicalCardPage({
const onCodeInput = (text: string) => {
setFormError('');

if (cardError) {
CardSettings.clearCardListErrors(cardID);
if (cardError && inactiveCard?.cardID) {
CardSettings.clearCardListErrors(inactiveCard?.cardID);
}

setLastFourDigits(text);
Expand All @@ -109,18 +108,21 @@ function ActivatePhysicalCardPage({
setFormError('activateCardPage.error.thatDidntMatch');
return;
}
if (inactiveCard?.cardID === undefined) {
return;
}

CardSettings.activatePhysicalExpensifyCard(lastFourDigits, cardID);
}, [lastFourDigits, cardID]);
CardSettings.activatePhysicalExpensifyCard(lastFourDigits, inactiveCard?.cardID);
}, [lastFourDigits, inactiveCard?.cardID]);

if (isEmptyObject(physicalCard)) {
if (isEmptyObject(inactiveCard)) {
return <NotFoundPage />;
}

return (
<IllustratedHeaderPageLayout
title={translate('activateCardPage.activateCard')}
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain))}
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID))}
backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.PREFERENCES.ROOT].backgroundColor}
illustration={LottieAnimations.Magician}
scrollViewContainerStyles={[styles.mnh100]}
Expand Down Expand Up @@ -148,7 +150,7 @@ function ActivatePhysicalCardPage({
<Button
success
isDisabled={isOffline}
isLoading={physicalCard?.isLoading}
isLoading={inactiveCard?.isLoading}
medium={isExtraSmallScreenHeight}
large={!isExtraSmallScreenHeight}
style={[styles.w100, styles.p5, styles.mtAuto]}
Expand Down
27 changes: 12 additions & 15 deletions src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,24 +109,25 @@ function BaseGetPhysicalCard({
const styles = useThemeStyles();
const isRouteSet = useRef(false);

const domainCards = CardUtils.getDomainCards(cardList)[domain] || [];
const cardToBeIssued = domainCards.find((card) => !card?.nameValuePairs?.isVirtual && card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED);
const cardID = cardToBeIssued?.cardID.toString() ?? '';

useEffect(() => {
if (isRouteSet.current || !privatePersonalDetails || !cardList) {
return;
}

const domainCards = CardUtils.getDomainCards(cardList)[domain] || [];
const physicalCard = domainCards.find((card) => !card?.nameValuePairs?.isVirtual);

// When there are no cards for the specified domain, user is redirected to the wallet page
if (domainCards.length === 0) {
if (domainCards.length === 0 || !cardToBeIssued) {
Navigation.goBack(ROUTES.SETTINGS_WALLET);
return;
}

// When there's no physical card or it exists but it doesn't have the required state for this flow,
// redirect user to the espensify card page
if (!physicalCard || physicalCard.state !== CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED) {
Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain));
if (cardToBeIssued.state !== CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED) {
Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardToBeIssued.cardID.toString()));
return;
}

Expand All @@ -141,25 +142,21 @@ function BaseGetPhysicalCard({
// Redirect user to previous steps of the flow if he hasn't finished them yet
GetPhysicalCardUtils.setCurrentRoute(currentRoute, domain, GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues));
isRouteSet.current = true;
}, [cardList, currentRoute, domain, draftValues, loginList, privatePersonalDetails]);
}, [cardList, currentRoute, domain, domainCards.length, draftValues, loginList, cardToBeIssued, privatePersonalDetails]);

const onSubmit = useCallback(() => {
const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues);
// If the current step of the get physical card flow is the confirmation page
if (isConfirmation) {
const domainCards = CardUtils.getDomainCards(cardList)[domain];
const physicalCard = domainCards.find((card) => !card?.nameValuePairs?.isVirtual);
const cardID = physicalCard?.cardID ?? 0;

Wallet.requestPhysicalExpensifyCard(cardID, session?.authToken ?? '', updatedPrivatePersonalDetails);
Wallet.requestPhysicalExpensifyCard(cardToBeIssued?.cardID ?? 0, session?.authToken ?? '', updatedPrivatePersonalDetails);
// Form draft data needs to be erased when the flow is complete,
// so that no stale data is left on Onyx
FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM);
Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain));
Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID.toString()));
return;
}
GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, updatedPrivatePersonalDetails);
}, [cardList, domain, draftValues, isConfirmation, session?.authToken]);
}, [cardID, cardToBeIssued?.cardID, domain, draftValues, isConfirmation, session?.authToken]);
return (
<ScreenWrapper
shouldEnablePickerAvoiding={false}
Expand All @@ -168,7 +165,7 @@ function BaseGetPhysicalCard({
>
<HeaderWithBackButton
title={title}
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain))}
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID))}
/>
<Text style={[styles.textHeadline, styles.mh5, styles.mb5]}>{headline}</Text>
{renderContent({onSubmit, submitButtonText, children, onValidate})}
Expand Down
Loading

0 comments on commit 6a37cd8

Please sign in to comment.