Skip to content

Commit 72917fe

Browse files
authored
Merge pull request Expensify#82376 from DylanDylann/release-1-set-pin
Set PIN
2 parents 221147f + f474f9a commit 72917fe

46 files changed

Lines changed: 1076 additions & 108 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/CONST/index.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2878,11 +2878,13 @@ const CONST = {
28782878

28792879
MISSING_PERSONAL_DETAILS: {
28802880
STEP_INDEX_LIST: ['1', '2', '3', '4'],
2881+
STEP_INDEX_LIST_WITH_PIN: ['1', '2', '3', '4', '5'],
28812882
PAGE_NAME: {
28822883
LEGAL_NAME: 'legal-name',
28832884
DATE_OF_BIRTH: 'date-of-birth',
28842885
ADDRESS: 'address',
28852886
PHONE_NUMBER: 'phone-number',
2887+
PIN: 'pin',
28862888
CONFIRM: 'confirm',
28872889
},
28882890
},
@@ -2902,6 +2904,14 @@ const CONST = {
29022904
PHONE_NUMBER: 3,
29032905
CONFIRM: 4,
29042906
},
2907+
MAPPING_WITH_PIN: {
2908+
LEGAL_NAME: 0,
2909+
DATE_OF_BIRTH: 1,
2910+
ADDRESS: 2,
2911+
PHONE_NUMBER: 3,
2912+
PIN: 4,
2913+
CONFIRM: 5,
2914+
},
29052915
INDEX_LIST: ['1', '2', '3', '4'],
29062916
},
29072917

@@ -3774,6 +3784,42 @@ const CONST = {
37743784
DAMAGED: 'damaged',
37753785
},
37763786
MANAGE_EXPENSIFY_CARDS_ARTICLE_LINK: 'https://help.expensify.com/articles/new-expensify/expensify-card/Manage-Expensify-Cards',
3787+
PIN: {
3788+
LENGTH: 4,
3789+
INVALID_PINS: [
3790+
'0000',
3791+
'1111',
3792+
'2222',
3793+
'3333',
3794+
'4444',
3795+
'5555',
3796+
'6666',
3797+
'7777',
3798+
'8888',
3799+
'9999',
3800+
'1234',
3801+
'2345',
3802+
'3456',
3803+
'4567',
3804+
'5678',
3805+
'6789',
3806+
'7890',
3807+
'0123',
3808+
'0987',
3809+
'9876',
3810+
'8765',
3811+
'7654',
3812+
'6543',
3813+
'5432',
3814+
'4321',
3815+
'3210',
3816+
'1212',
3817+
'1004',
3818+
'6969',
3819+
'2000',
3820+
'2015',
3821+
],
3822+
},
37773823
},
37783824
PERSONAL_CARDS: {
37793825
FEED_KEY_SEPARATOR: '#',

src/ROUTES.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3415,15 +3415,18 @@ const ROUTES = {
34153415
getRoute: (policyID: string) => `restricted-action/workspace/${policyID}` as const,
34163416
},
34173417
MISSING_PERSONAL_DETAILS: {
3418-
route: 'missing-personal-details/:subPage?/:action?',
3419-
getRoute: (subPage?: string, action?: 'edit') => {
3418+
route: 'missing-personal-details/:cardID/:subPage?/:action?',
3419+
getRoute: (cardID: string, subPage?: string, action?: 'edit') => {
34203420
if (!subPage) {
3421-
return 'missing-personal-details' as const;
3421+
return `missing-personal-details/${cardID}` as const;
34223422
}
3423-
return `missing-personal-details/${subPage}${action ? `/${action}` : ''}` as const;
3423+
return `missing-personal-details/${cardID}/${subPage}${action ? `/${action}` : ''}` as const;
34243424
},
34253425
},
3426-
MISSING_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE: 'missing-personal-details/confirm-magic-code',
3426+
MISSING_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE: {
3427+
route: 'missing-personal-details/:cardID/confirm-magic-code',
3428+
getRoute: (cardID: string) => `missing-personal-details/${cardID}/confirm-magic-code` as const,
3429+
},
34273430
POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR: {
34283431
route: 'workspaces/:policyID/accounting/netsuite/subsidiary-selector',
34293432
getRoute: (policyID: string | undefined) => {

src/components/MagicCodeInput.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ type MagicCodeInputProps = {
107107

108108
/** Reference to the outer element */
109109
ref?: ForwardedRef<MagicCodeInputHandle>;
110+
111+
/** Whether to mask the input characters (display as dots) */
112+
secureTextEntry?: boolean;
110113
};
111114

112115
type MagicCodeInputHandle = {
@@ -157,6 +160,7 @@ function MagicCodeInput({
157160
testID = '',
158161
accessibilityLabel,
159162
ref,
163+
secureTextEntry = false,
160164
}: MagicCodeInputProps) {
161165
const styles = useThemeStyles();
162166
const StyleUtils = useStyleUtils();
@@ -502,6 +506,7 @@ function MagicCodeInput({
502506
</GestureDetector>
503507
{getInputPlaceholderSlots(maxLength).map((index) => {
504508
const char = decomposeString(value, maxLength).at(index)?.trim() ?? '';
509+
const displayChar = secureTextEntry && char ? '•' : char;
505510
const cursorMargin = char ? {marginLeft: 2} : {};
506511
const isFocused = focusedIndex === index;
507512

@@ -521,7 +526,7 @@ function MagicCodeInput({
521526
]}
522527
>
523528
<View style={styles.magicCodeInputValueContainer}>
524-
<Text style={[styles.magicCodeInput, styles.textAlignCenter]}>{char}</Text>
529+
<Text style={[styles.magicCodeInput, styles.textAlignCenter]}>{displayChar}</Text>
525530
{isFocused && !isDisableKeyboard && (
526531
<View
527532
style={[styles.magicCodeInputCursorContainer]}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React from 'react';
2+
import createScreenWithDefaults from '@components/MultifactorAuthentication/components/OutcomeScreen/createScreenWithDefaults';
3+
import {DefaultClientFailureScreen} from '@components/MultifactorAuthentication/components/OutcomeScreen/FailureScreen/defaultScreens';
4+
import type {
5+
MultifactorAuthenticationScenario,
6+
MultifactorAuthenticationScenarioAdditionalParams,
7+
MultifactorAuthenticationScenarioCustomConfig,
8+
} from '@components/MultifactorAuthentication/config/types';
9+
import {clearDraftValues} from '@libs/actions/FormActions';
10+
import {setPersonalDetailsAndShipExpensifyCardsWithPIN} from '@libs/actions/MultifactorAuthentication';
11+
import Navigation from '@libs/Navigation/Navigation';
12+
import CONST from '@src/CONST';
13+
import ONYXKEYS from '@src/ONYXKEYS';
14+
import ROUTES from '@src/ROUTES';
15+
16+
/**
17+
* Payload type for the SET_PIN_ORDER_CARD scenario.
18+
* Contains personal details and PIN required for UK/EU card ordering.
19+
*/
20+
type Payload = {
21+
legalFirstName: string;
22+
legalLastName: string;
23+
phoneNumber: string;
24+
addressCity: string;
25+
addressStreet: string;
26+
addressStreet2: string;
27+
addressZip: string;
28+
addressCountry: string;
29+
addressState: string;
30+
dob: string;
31+
pin: string;
32+
cardID: string;
33+
};
34+
35+
/**
36+
* Type guard to verify the payload is a SetPinOrderCard payload.
37+
*/
38+
function isSetPinOrderCardPayload(payload: MultifactorAuthenticationScenarioAdditionalParams<MultifactorAuthenticationScenario> | undefined): payload is Payload {
39+
return !!payload && 'cardID' in payload && 'pin' in payload;
40+
}
41+
42+
const AuthenticationCanceledFailureScreen = createScreenWithDefaults(
43+
DefaultClientFailureScreen,
44+
{
45+
subtitle: 'multifactorAuthentication.setPin.didNotShipCard',
46+
},
47+
'AuthenticationCanceledFailureScreen',
48+
);
49+
50+
/**
51+
* Configuration for the SET_PIN_ORDER_CARD multifactor authentication scenario.
52+
* This scenario is used when a UK/EU cardholder sets their PIN during the card ordering process.
53+
*
54+
* Callback behavior per design doc:
55+
* - Success: Navigate to ExpensifyCardPage and return SKIP_OUTCOME_SCREEN
56+
* - Invalid PIN/personal details error: Display error in UI and return SKIP_OUTCOME_SCREEN
57+
* - Authentication failure: Return SHOW_OUTCOME_SCREEN to show failure screen
58+
*/
59+
export default {
60+
allowedAuthenticationMethods: [CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS],
61+
action: setPersonalDetailsAndShipExpensifyCardsWithPIN,
62+
63+
callback: async (isSuccessful, _callbackInput, payload) => {
64+
if (isSuccessful && isSetPinOrderCardPayload(payload)) {
65+
clearDraftValues(ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM);
66+
Navigation.closeRHPFlow();
67+
Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAIN_CARD.getRoute(String(payload.cardID)));
68+
return CONST.MULTIFACTOR_AUTHENTICATION.CALLBACK_RESPONSE.SKIP_OUTCOME_SCREEN;
69+
}
70+
71+
return CONST.MULTIFACTOR_AUTHENTICATION.CALLBACK_RESPONSE.SHOW_OUTCOME_SCREEN;
72+
},
73+
74+
failureScreens: {
75+
[CONST.MULTIFACTOR_AUTHENTICATION.REASON.EXPO.CANCELED]: <AuthenticationCanceledFailureScreen />,
76+
},
77+
} as const satisfies MultifactorAuthenticationScenarioCustomConfig<Payload>;
78+
79+
export type {Payload};

src/components/MultifactorAuthentication/config/scenarios/index.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,16 @@ import type {Payload as AuthorizeTransactionPayload} from './AuthorizeTransactio
55
import AuthorizeTransaction from './AuthorizeTransaction';
66
import BiometricsTest from './BiometricsTest';
77
import {customConfig} from './DefaultUserInterface';
8+
import type {Payload as SetPinOrderCardPayload} from './SetPinOrderCard';
9+
import SetPinOrderCard from './SetPinOrderCard';
810

911
/**
1012
* Payload types for multifactor authentication scenarios.
11-
* Since the BiometricsTest does not require any payload, it is an empty object for now.
12-
* The AuthorizeTransaction Scenario will change it, as it needs the transactionID to be provided as well.
13-
*
14-
* {
15-
* "AUTHORIZE-TRANSACTION": {
16-
* transactionID: string;
17-
* }
18-
* }
13+
* Each scenario that requires additional parameters should have its payload type defined here.
1914
*/
2015
type Payloads = {
2116
[CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.BIOMETRICS_TEST]: EmptyObject;
17+
[CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.SET_PIN_ORDER_CARD]: SetPinOrderCardPayload;
2218
[CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.AUTHORIZE_TRANSACTION]: AuthorizeTransactionPayload;
2319
};
2420

@@ -27,6 +23,7 @@ type Payloads = {
2723
*/
2824
const Configs = {
2925
[CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.BIOMETRICS_TEST]: customConfig(BiometricsTest),
26+
[CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.SET_PIN_ORDER_CARD]: customConfig(SetPinOrderCard),
3027
[CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.AUTHORIZE_TRANSACTION]: customConfig(AuthorizeTransaction),
3128
} as const satisfies MultifactorAuthenticationScenarioConfigRecord;
3229

src/components/MultifactorAuthentication/config/scenarios/names.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99
const SCENARIO_NAMES = {
1010
BIOMETRICS_TEST: 'BIOMETRICS-TEST',
11+
SET_PIN_ORDER_CARD: 'SET-PIN-ORDER-CARD',
1112
AUTHORIZE_TRANSACTION: 'AUTHORIZE-TRANSACTION',
1213
} as const;
1314

src/components/MultifactorAuthentication/config/types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ type MultifactorAuthenticationScenarioPureMethod<T extends Record<string, unknow
9090
type MultifactorAuthenticationScenarioBase<T extends Record<string, unknown> = EmptyObject> = {
9191
action: MultifactorAuthenticationScenarioPureMethod<T>;
9292
allowedAuthenticationMethods: Array<ValueOf<typeof CONST.MULTIFACTOR_AUTHENTICATION.TYPE>>;
93-
screen: MultifactorAuthenticationScreen;
93+
screen?: MultifactorAuthenticationScreen;
9494

9595
/**
9696
* Whether the scenario does not require any additional parameters except for the native biometrics data.
@@ -195,6 +195,15 @@ type MultifactorAuthenticationScenarioParameters = {
195195
*/
196196
type MultifactorAuthenticationScenario = ValueOf<typeof CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO>;
197197

198+
/**
199+
* Converts a scenario's parameters for API use by replacing signedChallenge with its stringified form.
200+
* The signedChallenge is validated as a structured object in the action layer, but needs to
201+
* JSON.stringify when sent to the API.
202+
*/
203+
type MultifactorAuthenticationAPIParams<T extends MultifactorAuthenticationScenario> = Omit<MultifactorAuthenticationScenarioParameters[T], 'signedChallenge'> & {
204+
signedChallenge: string;
205+
};
206+
198207
export type {
199208
MultifactorAuthenticationPrompt,
200209
MultifactorAuthenticationModal,
@@ -211,5 +220,6 @@ export type {
211220
MultifactorAuthenticationDefaultUIConfig,
212221
MultifactorAuthenticationCancelConfirm,
213222
MultifactorAuthenticationScenarioCustomConfig,
223+
MultifactorAuthenticationAPIParams,
214224
FailureScreenOverrides,
215225
};

src/components/ReportActionItem/IssueCardMessage.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function IssueCardMessage({action, policyID, shouldNavigateToCardDetails}: Issue
3636
const cardList = useNonPersonalCardList();
3737
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS);
3838
const companyCard = cardList?.[(getOriginalMessage(action) as IssueNewCardOriginalMessage)?.cardID];
39-
const shouldShowAddMissingDetailsButton = isAssigneeCurrentUser && shouldShowAddMissingDetails(action?.actionName, privatePersonalDetails);
39+
const shouldShowAddMissingDetailsButton = !!expensifyCard?.cardID && isAssigneeCurrentUser && shouldShowAddMissingDetails(action?.actionName, privatePersonalDetails);
4040
const shouldShowActivateButton = isAssigneeCurrentUser && shouldShowActivateCard(action?.actionName, expensifyCard, privatePersonalDetails);
4141

4242
const route = useRoute<PlatformStackRouteProp<ReportsSplitNavigatorParamList, typeof SCREENS.REPORT>>();
@@ -48,7 +48,9 @@ function IssueCardMessage({action, policyID, shouldNavigateToCardDetails}: Issue
4848
/>
4949
{shouldShowAddMissingDetailsButton && (
5050
<Button
51-
onPress={() => Navigation.navigate(ROUTES.MISSING_PERSONAL_DETAILS.getRoute())}
51+
onPress={() => {
52+
Navigation.navigate(ROUTES.MISSING_PERSONAL_DETAILS.getRoute(String(expensifyCard.cardID)));
53+
}}
5254
success
5355
style={[styles.alignSelfStart, styles.mt3]}
5456
text={translate('workspace.expensifyCard.addShippingDetails')}

src/languages/de.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,7 @@ const translations: TranslationDeepObject<typeof en> = {
749749
pleaseDownloadMobileApp: `Diese Aktion wird auf deinem Gerät nicht unterstützt. Bitte lade die Expensify-App aus dem <a href="${CONST.APP_DOWNLOAD_LINKS.IOS}">App Store</a> oder dem <a href="${CONST.APP_DOWNLOAD_LINKS.ANDROID}">Google Play Store</a> herunter und versuche es erneut.`,
750750
},
751751
verificationFailed: 'Überprüfung fehlgeschlagen',
752+
setPin: {didNotShipCard: 'Wir haben Ihre Karte nicht versendet. Bitte versuchen Sie es erneut.'},
752753
},
753754
validateCodeModal: {
754755
successfulSignInTitle: dedent(`
@@ -2365,6 +2366,14 @@ const translations: TranslationDeepObject<typeof en> = {
23652366

23662367
${amount} für ${merchant} – ${date}`,
23672368
},
2369+
setYourPin: 'Legen Sie Ihre PIN fest.',
2370+
confirmYourPin: 'Bestätigen Sie Ihre PIN.',
2371+
pinMustBeFourDigits: 'Die PIN muss genau 4 Ziffern lang sein.',
2372+
invalidPin: 'Bitte wählen Sie eine sicherere PIN.',
2373+
pinMismatch: 'PINs stimmen nicht überein. Bitte versuchen Sie es erneut.',
2374+
revealPin: 'PIN anzeigen',
2375+
hidePin: 'PIN ausblenden',
2376+
pin: 'PIN',
23682377
freezeCard: 'Karte sperren',
23692378
unfreeze: 'Entsperren',
23702379
unfreezeCard: 'Karte entsperren',

src/languages/en.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,9 @@ const translations = {
766766
return `${displayCount} other ${otherDeviceCount === 1 ? 'device' : 'devices'}`;
767767
},
768768
},
769+
setPin: {
770+
didNotShipCard: "We didn't ship your card. Please try again.",
771+
},
769772
},
770773
validateCodeModal: {
771774
successfulSignInTitle: dedent(`
@@ -2395,6 +2398,14 @@ const translations = {
23952398
alertMessage: ({cardLastFour, amount, merchant, date}: {cardLastFour: string; amount: string; merchant: string; date: string}) =>
23962399
`identified suspicious activity on card ending in ${cardLastFour}. Do you recognize this charge?\n\n${amount} for ${merchant} - ${date}`,
23972400
},
2401+
setYourPin: 'Set the PIN for your card.',
2402+
confirmYourPin: 'Enter your PIN again to confirm.',
2403+
pinMustBeFourDigits: 'PIN must be exactly 4 digits.',
2404+
invalidPin: 'Please choose a more secure PIN.',
2405+
pinMismatch: 'PINs do not match. Please try again.',
2406+
revealPin: 'Reveal PIN',
2407+
hidePin: 'Hide PIN',
2408+
pin: 'PIN',
23982409
freezeCard: 'Freeze card',
23992410
unfreeze: 'Unfreeze',
24002411
unfreezeCard: 'Unfreeze card',

0 commit comments

Comments
 (0)