Skip to content

Commit

Permalink
Merge pull request #28141 from JKobrynski/addReportCardLostOrDamaged
Browse files Browse the repository at this point in the history
Add report card lost or damaged
  • Loading branch information
marcaaron authored Oct 18, 2023
2 parents c09eb86 + a1c8c74 commit a03ab51
Show file tree
Hide file tree
Showing 13 changed files with 422 additions and 9 deletions.
2 changes: 2 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ const ONYXKEYS = {
PRIVATE_NOTES_FORM: 'privateNotesForm',
I_KNOW_A_TEACHER_FORM: 'iKnowTeacherForm',
INTRO_SCHOOL_PRINCIPAL_FORM: 'introSchoolPrincipalForm',
REPORT_PHYSICAL_CARD_FORM: 'requestPhysicalCardForm',
REPORT_VIRTUAL_CARD_FRAUD: 'reportVirtualCardFraudForm',
},
} as const;
Expand Down Expand Up @@ -430,6 +431,7 @@ type OnyxValues = {
[ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: OnyxTypes.Form;
[ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_AFTER_FORM]: OnyxTypes.Form;
[ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM]: OnyxTypes.Form;
[ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: OnyxTypes.Form;
};

type OnyxKeyValue<TOnyxKey extends (OnyxKey | OnyxCollectionKey) & keyof OnyxValues> = OnyxEntry<OnyxValues[TOnyxKey]>;
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ export default {
},
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`,
},
SETTINGS_WALLET_CARD_ACTIVATE: {
route: 'settings/wallet/card/:domain/activate',
getRoute: (domain: string) => `settings/wallet/card/${domain}/activate`,
Expand Down
7 changes: 6 additions & 1 deletion src/components/SelectCircle.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@ import themeColors from '../styles/themes/default';
const propTypes = {
/** Should we show the checkmark inside the circle */
isChecked: PropTypes.bool,

/** Additional styles to pass to SelectCircle */
// eslint-disable-next-line react/forbid-prop-types
styles: PropTypes.arrayOf(PropTypes.object),
};

const defaultProps = {
isChecked: false,
styles: [],
};

function SelectCircle(props) {
return (
<View style={[styles.selectCircle, styles.alignSelfCenter]}>
<View style={[styles.selectCircle, styles.alignSelfCenter, ...props.styles]}>
{props.isChecked && (
<Icon
src={Expensicons.Checkmark}
Expand Down
67 changes: 67 additions & 0 deletions src/components/SingleOptionSelector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'underscore';
import {View} from 'react-native';
import SelectCircle from './SelectCircle';
import styles from '../styles/styles';
import CONST from '../CONST';
import Text from './Text';
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
import withLocalize, {withLocalizePropTypes} from './withLocalize';

const propTypes = {
/** Array of options for the selector, key is a unique identifier, label is a localize key that will be translated and displayed */
options: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string,
label: PropTypes.string,
}),
),

/** Key of the option that is currently selected */
selectedOptionKey: PropTypes.string,

/** Function to be called when an option is selected */
onSelectOption: PropTypes.func,
...withLocalizePropTypes,
};

const defaultProps = {
options: [],
selectedOptionKey: undefined,
onSelectOption: () => {},
};

function SingleOptionSelector({options, selectedOptionKey, onSelectOption, translate}) {
return (
<View style={styles.pt4}>
{_.map(options, (option) => (
<View
style={styles.flexRow}
key={option.key}
>
<PressableWithoutFeedback
style={styles.singleOptionSelectorRow}
onPress={() => onSelectOption(option)}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
accessibilityState={{checked: selectedOptionKey === option.key}}
aria-checked={selectedOptionKey === option.key}
accessibilityLabel={option.label}
>
<SelectCircle
isChecked={selectedOptionKey ? selectedOptionKey === option.key : false}
styles={[styles.ml0, styles.singleOptionSelectorCircle]}
/>
<Text>{translate(option.label)}</Text>
</PressableWithoutFeedback>
</View>
))}
</View>
);
}

SingleOptionSelector.propTypes = propTypes;
SingleOptionSelector.defaultProps = defaultProps;
SingleOptionSelector.displayName = 'SingleOptionSelector';

export default withLocalize(SingleOptionSelector);
14 changes: 14 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1874,6 +1874,20 @@ export default {
selectSuggestedAddress: 'Please select a suggested address or use current location',
},
},
reportCardLostOrDamaged: {
report: 'Report physical card loss / damage',
screenTitle: 'Report card lost or damaged',
nextButtonLabel: 'Next',
reasonTitle: 'Why do you need a new card?',
cardDamaged: 'My card was damaged',
cardLostOrStolen: 'My card was lost or stolen',
confirmAddressTitle: "Please confirm the address below is where you'd like us to send your new card.",
currentCardInfo: 'Your current card will be permanently deactivated as soon as your order is placed. Most cards arrive in a few business days.',
address: 'Address',
deactivateCardButton: 'Deactivate card',
addressError: 'Address is required',
reasonError: 'Reason is required',
},
eReceipt: {
guaranteed: 'Guaranteed eReceipt',
transactionDate: 'Transaction date',
Expand Down
14 changes: 14 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2359,6 +2359,20 @@ export default {
selectSuggestedAddress: 'Por favor, selecciona una dirección sugerida o usa la ubicación actual.',
},
},
reportCardLostOrDamaged: {
report: 'Notificar la pérdida / daño de la tarjeta física',
screenTitle: 'Notificar la pérdida o deterioro de la tarjeta',
nextButtonLabel: 'Siguiente',
reasonTitle: '¿Por qué necesitas una tarjeta nueva?',
cardDamaged: 'Mi tarjeta está dañada',
cardLostOrStolen: 'He perdido o me han robado la tarjeta',
confirmAddressTitle: 'Confirma que la dirección que aparece a continuación es a la que deseas que te enviemos tu nueva tarjeta.',
currentCardInfo: 'La tarjeta actual se desactivará permanentemente en cuanto se realice el pedido. La mayoría de las tarjetas llegan en unos pocos días laborables.',
address: 'Dirección',
deactivateCardButton: 'Desactivar tarjeta',
addressError: 'La dirección es obligatoria',
reasonError: 'Se requiere justificación',
},
eReceipt: {
guaranteed: 'eRecibo garantizado',
transactionDate: 'Fecha de transacción',
Expand Down
12 changes: 11 additions & 1 deletion src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import CONST from '../CONST';
import * as Localize from './Localize';
import * as OnyxTypes from '../types/onyx';
import ONYXKEYS, {OnyxValues} from '../ONYXKEYS';
import {Card} from '../types/onyx';

let allCards: OnyxValues[typeof ONYXKEYS.CARD_LIST] = {};
Onyx.connect({
Expand Down Expand Up @@ -88,4 +89,13 @@ function maskCard(lastFour = ''): string {
return maskedString.replace(/(.{4})/g, '$1 ').trim();
}

export {isExpensifyCard, getDomainCards, getMonthFromExpirationDateString, getYearFromExpirationDateString, maskCard, getCardDescription};
/**
* Finds physical card in a list of cards
*
* @returns a physical card object (or undefined if none is found)
*/
function findPhysicalCard(cards: Card[]) {
return cards.find((card) => !card.isVirtual);
}

export {isExpensifyCard, getDomainCards, getMonthFromExpirationDateString, getYearFromExpirationDateString, maskCard, getCardDescription, findPhysicalCard};
1 change: 1 addition & 0 deletions src/libs/Navigation/AppNavigator/ModalStackNavigators.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ const SettingsModalStackNavigator = createModalStackNavigator({
ReimbursementAccount: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default,
GetAssistance: () => require('../../../pages/GetAssistancePage').default,
Settings_TwoFactorAuth: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default,
Settings_ReportCardLostOrDamaged: () => require('../../../pages/settings/Wallet/ReportCardLostPage').default,
KeyboardShortcuts: () => require('../../../pages/KeyboardShortcutsPage').default,
});

Expand Down
4 changes: 4 additions & 0 deletions src/libs/Navigation/linkingConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export default {
path: ROUTES.SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT,
exact: true,
},
Settings_ReportCardLostOrDamaged: {
path: ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.route,
exact: true,
},
Settings_Wallet_Card_Activate: {
path: ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.route,
exact: true,
Expand Down
47 changes: 46 additions & 1 deletion src/libs/actions/Card.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,51 @@ function reportVirtualExpensifyCardFraud(cardID) {
);
}

/**
* Call the API to deactivate the card and request a new one
* @param {String} cardId - id of the card that is going to be replaced
* @param {String} reason - reason for replacement ('damaged' | 'stolen')
*/
function requestReplacementExpensifyCard(cardId, reason) {
API.write(
'RequestReplacementExpensifyCard',
{
cardId,
reason,
},
{
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM,
value: {
isLoading: true,
errors: null,
},
},
],
successData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM,
value: {
isLoading: false,
},
},
],
failureData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM,
value: {
isLoading: false,
},
},
],
},
);
}

/**
* Activates the physical Expensify card based on the last four digits of the card number
*
Expand Down Expand Up @@ -101,4 +146,4 @@ function clearCardListErrors(cardID) {
Onyx.merge(ONYXKEYS.CARD_LIST, {[cardID]: {errors: null, isLoading: false}});
}

export {reportVirtualExpensifyCardFraud, activatePhysicalExpensifyCard, clearCardListErrors};
export {requestReplacementExpensifyCard, activatePhysicalExpensifyCard, clearCardListErrors, reportVirtualExpensifyCardFraud};
21 changes: 15 additions & 6 deletions src/pages/settings/Wallet/ExpensifyCardPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as Expensicons from '../../../components/Icon/Expensicons';
import * as CardUtils from '../../../libs/CardUtils';
import Button from '../../../components/Button';
import CardDetails from './WalletPage/CardDetails';
import MenuItem from '../../../components/MenuItem';
import CONST from '../../../CONST';
import assignedCardPropTypes from './assignedCardPropTypes';

Expand Down Expand Up @@ -120,12 +121,20 @@ function ExpensifyCardPage({
</>
)}
{!_.isEmpty(physicalCard) && (
<MenuItemWithTopDescription
description={translate('cardPage.physicalCardNumber')}
title={CardUtils.maskCard(physicalCard.lastFourPAN)}
interactive={false}
titleStyle={styles.walletCardMenuItem}
/>
<>
<MenuItemWithTopDescription
description={translate('cardPage.physicalCardNumber')}
title={CardUtils.maskCard(physicalCard.lastFourPAN)}
interactive={false}
titleStyle={styles.walletCardMenuItem}
/>
<MenuItem
title={translate('reportCardLostOrDamaged.report')}
icon={Expensicons.Flag}
shouldShowRightIcon
onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.getRoute(domain))}
/>
</>
)}
</ScrollView>
{physicalCard.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED && (
Expand Down
Loading

0 comments on commit a03ab51

Please sign in to comment.