Skip to content
Merged
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
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,7 @@
"Warchoł",
"WDYR",
"webapps",
"webauthn",
"webcredentials",
"webrtc",
"welldone",
Expand Down
5 changes: 4 additions & 1 deletion src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {ValueOf} from 'type-fest';
import type {SearchFilterKey} from '@components/Search/types';
import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types';
import type {MileageRate} from '@libs/DistanceRequestUtils';
import MULTIFACTOR_AUTHENTICATION_VALUES from '@libs/MultifactorAuthentication/Biometrics/VALUES';
import addTrailingForwardSlash from '@libs/UrlUtils';
import variables from '@styles/variables';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -423,6 +424,8 @@ const CONST = {
MAX_AGE: 150,
},

MULTIFACTOR_AUTHENTICATION: MULTIFACTOR_AUTHENTICATION_VALUES,

DESKTOP_SHORTCUT_ACCELERATOR: {
PASTE_AND_MATCH_STYLE: 'Option+Shift+CmdOrCtrl+V',
PASTE_AS_PLAIN_TEXT: 'CmdOrCtrl+Shift+V',
Expand Down Expand Up @@ -5501,7 +5504,7 @@ const CONST = {
DISABLED: 'DISABLED',
DISABLE: 'DISABLE',
},
MULTIFACTOR_AUTHENTICATION_NOTIFICATION_TYPE: {
MULTIFACTOR_AUTHENTICATION_OUTCOME_TYPE: {
SUCCESS: 'success',
FAILURE: 'failure',
},
Expand Down
8 changes: 4 additions & 4 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3719,11 +3719,11 @@ const ROUTES = {
MULTIFACTOR_AUTHENTICATION_MAGIC_CODE: `${MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES.FACTOR}/magic-code`,
MULTIFACTOR_AUTHENTICATION_BIOMETRICS_TEST: 'multifactor-authentication/scenario/biometrics-test',

// The exact notification & prompt type will be added as a part of Multifactor Authentication config in another PR,
// The exact outcome & prompt type will be added as a part of Multifactor Authentication config in another PR,
// for now a string is accepted to avoid blocking this PR.
MULTIFACTOR_AUTHENTICATION_NOTIFICATION: {
route: 'multifactor-authentication/notification/:notificationType',
getRoute: (notificationType: ValueOf<typeof CONST.MULTIFACTOR_AUTHENTICATION_NOTIFICATION_TYPE>) => `multifactor-authentication/notification/${notificationType}` as const,
MULTIFACTOR_AUTHENTICATION_OUTCOME: {
route: 'multifactor-authentication/outcome/:outcomeType',
getRoute: (outcomeType: ValueOf<typeof CONST.MULTIFACTOR_AUTHENTICATION_OUTCOME_TYPE>) => `multifactor-authentication/outcome/${outcomeType}` as const,
},

MULTIFACTOR_AUTHENTICATION_PROMPT: {
Expand Down
2 changes: 1 addition & 1 deletion src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@ const SCREENS = {
MULTIFACTOR_AUTHENTICATION: {
MAGIC_CODE: 'Multifactor_Authentication_Magic_Code',
BIOMETRICS_TEST: 'Multifactor_Authentication_Biometrics_Test',
NOTIFICATION: 'Multifactor_Authentication_Notification',
OUTCOME: 'Multifactor_Authentication_Outcome',
PROMPT: 'Multifactor_Authentication_Prompt',
NOT_FOUND: 'Multifactor_Authentication_Not_Found',
},
Expand Down
12 changes: 12 additions & 0 deletions src/components/MultifactorAuthentication/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Configuration exports for multifactor authentication UI components and scenarios.
*/
import mapMultifactorAuthenticationOutcomes from './mapMultifactorAuthenticationOutcomes';
import MULTIFACTOR_AUTHENTICATION_SCENARIO_CONFIG from './scenarios';

const MULTIFACTOR_AUTHENTICATION_OUTCOME_MAP = mapMultifactorAuthenticationOutcomes(MULTIFACTOR_AUTHENTICATION_SCENARIO_CONFIG);

export {MULTIFACTOR_AUTHENTICATION_SCENARIO_CONFIG, MULTIFACTOR_AUTHENTICATION_OUTCOME_MAP};
export {default as MULTIFACTOR_AUTHENTICATION_PROMPT_UI} from './scenarios/prompts';
export {default as MULTIFACTOR_AUTHENTICATION_DEFAULT_UI} from './scenarios/DefaultUserInterface';
export type {Payloads as MultifactorAuthenticationScenarioPayload} from './scenarios';
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import StringUtils from '@libs/StringUtils';
import type {
MultifactorAuthenticationOutcomeMap,
MultifactorAuthenticationOutcomeOptions,
MultifactorAuthenticationOutcomeRecord,
MultifactorAuthenticationScenario,
MultifactorAuthenticationScenarioConfigRecord,
} from './types';

/**
* This utility module provides functions to map multifactor authentication scenario configurations
* to an outcome map with kebab-case keys.
*
* This allows outcome pages to reference the config based on its OutcomeType in url.
*
* e.g.
*
* {
* "BIOMETRICS-TEST": {
* // ...
* OUTCOMES: {
* success: {
* title: "...",
* // ...
* },
* failure: {
* title: "...",
* // ...
* },
* // ...
* }
* },
* "AUTHORIZE-TRANSACTION": {
* // ...
* }
* }
*
* is mapped to:
*
* {
* "biometrics-test-success": {
* title: "...",
* // ...
* },
* "biometrics-test-failure": {
* title: "...",
* // ...
* },
* "authorize-transaction-success": {
* // ...
* }
* // ...
* }
*/

/**
* Creates an outcome record from multifactor authentication scenario configuration.
* For details refer to the example above.
*/
const createOutcomeRecord = (mfaConfig: MultifactorAuthenticationScenarioConfigRecord): MultifactorAuthenticationOutcomeRecord => {
const entries = Object.entries({...mfaConfig});
return entries.reduce((record, [key, {OUTCOMES}]) => {
// eslint-disable-next-line no-param-reassign
record[key as MultifactorAuthenticationScenario] = {...OUTCOMES};
return record;
}, {} as MultifactorAuthenticationOutcomeRecord);
};

/**
* Creates an outcome key by combining scenario and outcome name in kebab-case format.
* e.g. a scenario key of "BIOMETRICS-TEST" and outcome name of "success" will produce "biometrics-test-success".
*/
const createOutcomeKey = (key: string, name: string) => {
const scenarioKebabCase = StringUtils.toLowerCase(key as MultifactorAuthenticationScenario);
const outcomeName = StringUtils.camelToKebabCase(name as MultifactorAuthenticationOutcomeOptions);

return `${scenarioKebabCase}-${outcomeName}` as const;
};

/**
* Maps multifactor authentication scenario configuration to an outcome map with kebab-case keys.
*/
const mapMultifactorAuthenticationOutcomes = (mfaConfig: MultifactorAuthenticationScenarioConfigRecord) => {
const recordEntries = Object.entries(createOutcomeRecord({...mfaConfig}));

const outcomes: Partial<MultifactorAuthenticationOutcomeMap> = {};

for (const [key, config] of recordEntries) {
for (const [name, ui] of Object.entries(config)) {
outcomes[createOutcomeKey(key, name)] = {...ui};
}
}

return outcomes as MultifactorAuthenticationOutcomeMap;
};

export default mapMultifactorAuthenticationOutcomes;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type {MultifactorAuthenticationScenarioCustomConfig} from '@components/MultifactorAuthentication/config/types';
import {troubleshootMultifactorAuthentication} from '@userActions/MultifactorAuthentication';
import CONST from '@src/CONST';
import SCREENS from '@src/SCREENS';

/**
* Configuration for the biometrics test multifactor authentication scenario.
*/
export default {
allowedAuthenticationMethods: [CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS],
action: troubleshootMultifactorAuthentication,
screen: SCREENS.MULTIFACTOR_AUTHENTICATION.BIOMETRICS_TEST,
pure: true,
OUTCOMES: {
success: {
headerTitle: 'multifactorAuthentication.biometricsTest.biometricsTest',
},
failure: {
headerTitle: 'multifactorAuthentication.biometricsTest.biometricsTest',
},
outOfTime: {
headerTitle: 'multifactorAuthentication.biometricsTest.biometricsTest',
},
},
} as const satisfies MultifactorAuthenticationScenarioCustomConfig;
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type {MultifactorAuthenticationDefaultUIConfig, MultifactorAuthenticationScenarioCustomConfig} from '@components/MultifactorAuthentication/config/types';
import NoEligibleMethodsDescription from '@components/MultifactorAuthentication/NoEligibleMethodsDescription';
// Spacing utilities are needed for icon padding configuration in outcomes defaults
// eslint-disable-next-line no-restricted-imports
import spacing from '@styles/utils/spacing';
import variables from '@styles/variables';

/**
* Default UI configuration for all multifactor authentication scenarios with modals and outcomes.
*/
const DEFAULT_CONFIG = {
OUTCOMES: {
success: {
illustration: 'OpenPadlock',
iconWidth: variables.openPadlockWidth,
iconHeight: variables.openPadlockHeight,
padding: spacing.p2,
headerTitle: 'multifactorAuthentication.biometricsTest.biometricsAuthentication',
title: 'multifactorAuthentication.biometricsTest.authenticationSuccessful',
description: 'multifactorAuthentication.biometricsTest.successfullyAuthenticatedUsing',
},
failure: {
illustration: 'HumptyDumpty',
iconWidth: variables.humptyDumptyWidth,
iconHeight: variables.humptyDumptyHeight,
padding: spacing.p0,
headerTitle: 'multifactorAuthentication.biometricsTest.biometricsAuthentication',
title: 'multifactorAuthentication.oops',
description: 'multifactorAuthentication.biometricsTest.yourAttemptWasUnsuccessful',
},
outOfTime: {
illustration: 'RunOutOfTime',
iconWidth: variables.runOutOfTimeWidth,
iconHeight: variables.runOutOfTimeHeight,
padding: spacing.p0,
headerTitle: 'multifactorAuthentication.biometricsTest.biometricsAuthentication',
title: 'multifactorAuthentication.youRanOutOfTime',
description: 'multifactorAuthentication.looksLikeYouRanOutOfTime',
},
noEligibleMethods: {
illustration: 'HumptyDumpty',
iconWidth: variables.humptyDumptyWidth,
iconHeight: variables.humptyDumptyHeight,
padding: spacing.p0,
headerTitle: 'multifactorAuthentication.biometricsTest.biometricsAuthentication',
title: 'multifactorAuthentication.biometricsTest.youCouldNotBeAuthenticated',
description: 'multifactorAuthentication.biometricsTest.youCouldNotBeAuthenticated',
customDescription: NoEligibleMethodsDescription,
},
},
MODALS: {
cancelConfirmation: {
title: 'common.areYouSure',
description: 'multifactorAuthentication.biometricsTest.areYouSureToReject',
confirmButtonText: 'multifactorAuthentication.biometricsTest.rejectAuthentication',
cancelButtonText: 'common.cancel',
},
},
nativePromptTitle: 'multifactorAuthentication.letsVerifyItsYou',
} as const satisfies MultifactorAuthenticationDefaultUIConfig;

/**
* Merges custom scenario configuration with default UI configuration for modals and outcomes.
*/
function customConfig<const T extends MultifactorAuthenticationScenarioCustomConfig<never>>(config: T) {
const MODALS = {
...DEFAULT_CONFIG.MODALS,
...config.MODALS,
cancelConfirmation: {
...DEFAULT_CONFIG.MODALS.cancelConfirmation,
...config.MODALS?.cancelConfirmation,
},
} as const;

const OUTCOMES = {
...DEFAULT_CONFIG.OUTCOMES,
...config.OUTCOMES,
success: {
...DEFAULT_CONFIG.OUTCOMES.success,
...config.OUTCOMES?.success,
},
failure: {
...DEFAULT_CONFIG.OUTCOMES.failure,
...config.OUTCOMES?.failure,
},
outOfTime: {
...DEFAULT_CONFIG.OUTCOMES.outOfTime,
...config.OUTCOMES?.outOfTime,
},
noEligibleMethods: {
...DEFAULT_CONFIG.OUTCOMES.noEligibleMethods,
...config.OUTCOMES?.noEligibleMethods,
},
} as const;

return {
...DEFAULT_CONFIG,
...config,
MODALS,
OUTCOMES,
} as const;
}

export default DEFAULT_CONFIG;
export {customConfig};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type {EmptyObject} from 'type-fest';
import type {MultifactorAuthenticationScenarioConfigRecord} from '@components/MultifactorAuthentication/config/types';
import CONST from '@src/CONST';
import BiometricsTest from './BiometricsTest';
import {customConfig} from './DefaultUserInterface';

/**
* Payload types for multifactor authentication scenarios.
* Since the BiometricsTest does not require any payload, it is an empty object for now.
* The AuthorizeTransaction Scenario will change it, as it needs the transactionID to be provided as well.
*
* {
* "AUTHORIZE-TRANSACTION": {
* transactionID: string;
* }
* }
*/
type Payloads = EmptyObject;

/**
* Configuration records for all multifactor authentication scenarios.
*/
const Configs = {
[CONST.MULTIFACTOR_AUTHENTICATION.SCENARIO.BIOMETRICS_TEST]: customConfig(BiometricsTest),
} as const satisfies MultifactorAuthenticationScenarioConfigRecord;

export default Configs;
export type {Payloads};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Multifactor authentication scenario names.
*
* The names need to be a kebab-case string to satisfy the requirements of the URL schema.
* Moreover, they are exported to a separate file to avoid circular dependencies
* as the Multifactor Authentication configs imports SCREENS, actions, and other shared modules,
* and at the same time the config is imported in the CONSTs.
*/
const SCENARIO_NAMES = {
BIOMETRICS_TEST: 'BIOMETRICS-TEST',
} as const;

/**
* Prompt identifiers for multifactor authentication scenarios.
*/
const PROMPT_NAMES = {
ENABLE_BIOMETRICS: 'enable-biometrics',
};

export {SCENARIO_NAMES, PROMPT_NAMES};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import LottieAnimations from '@components/LottieAnimations';
import type {MultifactorAuthenticationPrompt} from '@components/MultifactorAuthentication/config/types';
import VALUES from '@libs/MultifactorAuthentication/Biometrics/VALUES';

/**
* Configuration for multifactor authentication prompt UI with animations and translations.
* Exported to a separate file to avoid circular dependencies.
*/
export default {
[VALUES.PROMPT.ENABLE_BIOMETRICS]: {
animation: LottieAnimations.Fingerprint,
title: 'multifactorAuthentication.verifyYourself.biometrics',
subtitle: 'multifactorAuthentication.enableQuickVerification.biometrics',
},
} as const satisfies MultifactorAuthenticationPrompt;
Loading
Loading