From 31734d59d8becec50c6378e9a87a610679ead603 Mon Sep 17 00:00:00 2001 From: Mostafa Gamal <46829557+MosCD3@users.noreply.github.com> Date: Thu, 7 Nov 2024 02:49:34 -0500 Subject: [PATCH] chore: display error messages for PIN screens inline mode (#1253) Signed-off-by: Mostafa Gamal Signed-off-by: Mostafa Gamal <46829557+MosCD3@users.noreply.github.com> Signed-off-by: Mostafa Youssef Co-authored-by: Mostafa Youssef --- packages/legacy/app/App.tsx | 90 ---- .../core/App/assets/img/error-filled.svg | 12 + .../core/App/assets/img/exclamation-mark.svg | 17 + .../App/components/inputs/InlineErrorText.tsx | 50 +++ .../core/App/components/inputs/PINInput.tsx | 108 +++-- packages/legacy/core/App/container-api.ts | 5 +- packages/legacy/core/App/container-impl.ts | 2 + packages/legacy/core/App/index.ts | 5 + .../legacy/core/App/screens/PINCreate.tsx | 409 ++++++++++-------- packages/legacy/core/App/screens/PINEnter.tsx | 29 +- packages/legacy/core/App/theme.ts | 49 +++ packages/legacy/core/App/types/error.ts | 13 + .../__snapshots__/PINEnter.test.tsx.snap | 22 +- 13 files changed, 479 insertions(+), 332 deletions(-) delete mode 100644 packages/legacy/app/App.tsx create mode 100644 packages/legacy/core/App/assets/img/error-filled.svg create mode 100644 packages/legacy/core/App/assets/img/exclamation-mark.svg create mode 100644 packages/legacy/core/App/components/inputs/InlineErrorText.tsx diff --git a/packages/legacy/app/App.tsx b/packages/legacy/app/App.tsx deleted file mode 100644 index 29855ebba0..0000000000 --- a/packages/legacy/app/App.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { - AgentProvider, - AnimatedComponentsProvider, - AuthProvider, - ConfigurationProvider, - ErrorModal, - InactivityWrapper, - NetInfo, - NetworkProvider, - RootStack, - StoreProvider, - ThemeProvider, - TourProvider, - animatedComponents, - credentialOfferTourSteps, - credentialsTourSteps, - defaultConfiguration, - homeTourSteps, - initLanguages, - initStoredLanguage, - proofRequestTourSteps, - theme, - toastConfig, - translationResources, -} from '@hyperledger/aries-bifold-core' -import * as React from 'react' -import { useEffect, useMemo } from 'react' -import { StatusBar } from 'react-native' -import { isTablet } from 'react-native-device-info' -import Orientation from 'react-native-orientation-locker' -import SplashScreen from 'react-native-splash-screen' -import Toast from 'react-native-toast-message' - -initLanguages(translationResources) - -const App = () => { - useMemo(() => { - initStoredLanguage().then() - }, []) - - useEffect(() => { - // Hide the native splash / loading screen so that our - // RN version can be displayed. - SplashScreen.hide() - }, []) - - if (!isTablet()) { - Orientation.lockToPortrait() - } - - return ( - - - - - - - - - - - - - - - - - ) -} - -export default App diff --git a/packages/legacy/core/App/assets/img/error-filled.svg b/packages/legacy/core/App/assets/img/error-filled.svg new file mode 100644 index 0000000000..0266d43e71 --- /dev/null +++ b/packages/legacy/core/App/assets/img/error-filled.svg @@ -0,0 +1,12 @@ + + + + error-filled + + + + + + + + \ No newline at end of file diff --git a/packages/legacy/core/App/assets/img/exclamation-mark.svg b/packages/legacy/core/App/assets/img/exclamation-mark.svg new file mode 100644 index 0000000000..7860f1348c --- /dev/null +++ b/packages/legacy/core/App/assets/img/exclamation-mark.svg @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/legacy/core/App/components/inputs/InlineErrorText.tsx b/packages/legacy/core/App/components/inputs/InlineErrorText.tsx new file mode 100644 index 0000000000..bc7ad81da5 --- /dev/null +++ b/packages/legacy/core/App/components/inputs/InlineErrorText.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import { View, StyleSheet, Text } from 'react-native' + +import { useTheme } from '../../contexts/theme' +import { SvgProps } from 'react-native-svg' +import { InlineErrorConfig } from '../../types/error' + +export enum InlineErrorType { + error, + warning, +} + +export interface InlineMessageProps { + message: string + inlineType: InlineErrorType + config: InlineErrorConfig +} + +const InlineErrorText: React.FC = ({ message, inlineType, config }) => { + const { InputInlineMessage } = useTheme() + const style = StyleSheet.create({ + container: { + flexDirection: 'row', + alignContent: 'center', + marginVertical: 5, + paddingRight: 20, + }, + icon: { marginRight: 4 }, + }) + + const color = + inlineType === InlineErrorType.warning + ? InputInlineMessage.inlineWarningText.color + : InputInlineMessage.inlineErrorText.color + + const props: SvgProps = { height: 16, width: 16, color: color, style: style.icon } + + return ( + + {inlineType === InlineErrorType.warning ? ( + + ) : ( + + )} + {message} + + ) +} + +export default InlineErrorText diff --git a/packages/legacy/core/App/components/inputs/PINInput.tsx b/packages/legacy/core/App/components/inputs/PINInput.tsx index 3d2d3dd42b..b72f26bbdc 100644 --- a/packages/legacy/core/App/components/inputs/PINInput.tsx +++ b/packages/legacy/core/App/components/inputs/PINInput.tsx @@ -7,6 +7,8 @@ import Icon from 'react-native-vector-icons/MaterialIcons' import { hitSlop, minPINLength } from '../../constants' import { useTheme } from '../../contexts/theme' import { testIdWithKey } from '../../utils/testable' +import InlineErrorText, { InlineMessageProps } from './InlineErrorText' +import { InlineErrorPosition } from '../../types/error' interface PINInputProps { label?: string @@ -14,10 +16,11 @@ interface PINInputProps { testID?: string accessibilityLabel?: string autoFocus?: boolean + inlineMessage?: InlineMessageProps } const PINInputComponent = ( - { label, onPINChanged, testID, accessibilityLabel, autoFocus = false }: PINInputProps, + { label, onPINChanged, testID, accessibilityLabel, autoFocus = false, inlineMessage }: PINInputProps, ref: Ref ) => { // const accessible = accessibilityLabel && accessibilityLabel !== '' ? true : false @@ -58,53 +61,70 @@ const PINInputComponent = ( paddingHorizontal: 10, }, }) + const content = () => ( + + + { + let child: React.ReactNode | string = '' + if (symbol) { + child = showPIN ? symbol : '●' // Show or hide PIN + } else if (isFocused) { + child = + } + return ( + + + {child} + + + ) + }} + autoFocus={autoFocus} + ref={ref} + /> + + setShowPIN(!showPIN)} + hitSlop={hitSlop} + > + + + + ) + const inlineMessageView = ({ message, inlineType, config }: InlineMessageProps) => ( + + ) + const inlineMessagePlaceholder = (placment: InlineErrorPosition) => { + if (inlineMessage && inlineMessage.config.position === placment) { + return inlineMessageView(inlineMessage) + } + //This is a fallback in case no position provided + if (inlineMessage && placment === InlineErrorPosition.Above && !inlineMessage.config.position) { + return inlineMessageView(inlineMessage) + } + } return ( {label && {label}} - - - { - let child: React.ReactNode | string = '' - if (symbol) { - child = showPIN ? symbol : '●' // Show or hide PIN - } else if (isFocused) { - child = - } - return ( - - - {child} - - - ) - }} - autoFocus={autoFocus} - ref={ref} - /> - - setShowPIN(!showPIN)} - hitSlop={hitSlop} - > - - - + {inlineMessagePlaceholder(InlineErrorPosition.Above)} + {content()} + {inlineMessagePlaceholder(InlineErrorPosition.Below)} ) } diff --git a/packages/legacy/core/App/container-api.ts b/packages/legacy/core/App/container-api.ts index 51953e3b4a..ed86209974 100644 --- a/packages/legacy/core/App/container-api.ts +++ b/packages/legacy/core/App/container-api.ts @@ -22,6 +22,7 @@ import { PINExplainerProps } from './screens/PINExplainer' import { CredentialListFooterProps } from './types/credential-list-footer' import { ContactListItemProps } from './components/listItems/ContactListItem' import { ContactCredentialListItemProps } from './components/listItems/ContactCredentialListItem' +import { InlineErrorConfig } from './types/error' export type FN_ONBOARDING_DONE = ( dispatch: React.Dispatch>, @@ -47,7 +48,7 @@ export const SCREEN_TOKENS = { SCREEN_SPLASH: 'screen.splash', SCREEN_SCAN: 'screen.scan', SCREEN_USE_BIOMETRY: 'screen.use-biometry', - SCREEN_PIN_EXPLAINER: 'screen.pin-explainer' + SCREEN_PIN_EXPLAINER: 'screen.pin-explainer', } as const export const NAV_TOKENS = { @@ -117,6 +118,7 @@ export const UTILITY_TOKENS = { export const CONFIG_TOKENS = { CONFIG: 'config', + INLINE_ERRORS: 'errors.inline', } as const export const TOKENS = { @@ -186,6 +188,7 @@ export type TokenMapping = { [TOKENS.COMPONENT_RECORD]: React.FC [TOKENS.COMPONENT_CONTACT_LIST_ITEM]: React.FC [TOKENS.COMPONENT_CONTACT_DETAILS_CRED_LIST_ITEM]: React.FC + [TOKENS.INLINE_ERRORS]: InlineErrorConfig [TOKENS.CUSTOM_NAV_STACK_1]: React.FC } diff --git a/packages/legacy/core/App/container-impl.ts b/packages/legacy/core/App/container-impl.ts index fb0eec56ee..5fa890bd53 100644 --- a/packages/legacy/core/App/container-impl.ts +++ b/packages/legacy/core/App/container-impl.ts @@ -49,6 +49,7 @@ import { Config } from './types/config' import { Locales } from './localization' import ContactListItem from './components/listItems/ContactListItem' import ContactCredentialListItem from './components/listItems/ContactCredentialListItem' +import { InlineErrorPosition } from './types/error' export const defaultConfig: Config = { PINSecurity: { rules: PINRules, displayHelper: false }, @@ -126,6 +127,7 @@ export class MainContainer implements Container { this._container.registerInstance(TOKENS.COMPONENT_CONTACT_DETAILS_CRED_LIST_ITEM, ContactCredentialListItem) this._container.registerInstance(TOKENS.CACHE_CRED_DEFS, []) this._container.registerInstance(TOKENS.CACHE_SCHEMAS, []) + this._container.registerInstance(TOKENS.INLINE_ERRORS, { enabled: true, position: InlineErrorPosition.Above }) this._container.registerInstance( TOKENS.FN_ONBOARDING_DONE, (dispatch: React.Dispatch>, navigation: StackNavigationProp) => { diff --git a/packages/legacy/core/App/index.ts b/packages/legacy/core/App/index.ts index 4296eedbc9..42fe4869ad 100644 --- a/packages/legacy/core/App/index.ts +++ b/packages/legacy/core/App/index.ts @@ -117,6 +117,11 @@ export type { Migration as MigrationState, Tours as ToursState, } from './types/state' + +export type { InlineMessageProps } from './components/inputs/InlineErrorText' + +export type { InlineErrorPosition } from './types/error' + export type { CredentialListFooterProps } export * from './container-api' export { MainContainer } from './container-impl' diff --git a/packages/legacy/core/App/screens/PINCreate.tsx b/packages/legacy/core/App/screens/PINCreate.tsx index 92b5b1528c..e33999049d 100644 --- a/packages/legacy/core/App/screens/PINCreate.tsx +++ b/packages/legacy/core/App/screens/PINCreate.tsx @@ -31,6 +31,7 @@ import { BifoldError } from '../types/error' import { AuthenticateStackParams, Screens } from '../types/navigators' import { PINCreationValidations, PINValidationsType } from '../utils/PINCreationValidation' import { testIdWithKey } from '../utils/testable' +import { InlineErrorType, InlineMessageProps } from '../components/inputs/InlineErrorText' interface PINCreateProps extends StackScreenProps { setAuthenticated: (status: boolean) => void @@ -57,11 +58,13 @@ const PINCreate: React.FC = ({ setAuthenticated, explainedStatus title: '', message: '', }) - const [explained, setExplained] = useState(explainedStatus); + const [explained, setExplained] = useState(explainedStatus) const iconSize = 24 const navigation = useNavigation>() const [store, dispatch] = useStore() const { t } = useTranslation() + const [inlineMessageField1, setInlineMessageField1] = useState() + const [inlineMessageField2, setInlineMessageField2] = useState() const { ColorPallet, TextTheme } = useTheme() const { ButtonLoading } = useAnimatedComponents() @@ -69,11 +72,12 @@ const PINCreate: React.FC = ({ setAuthenticated, explainedStatus const createPINButtonRef = useRef(null) const actionButtonLabel = updatePin ? t('PINCreate.ChangePIN') : t('PINCreate.CreatePIN') const actionButtonTestId = updatePin ? testIdWithKey('ChangePIN') : testIdWithKey('CreatePIN') - const [PINExplainer, PINCreateHeader, { PINSecurity }, Button] = useServices([ + const [PINExplainer, PINCreateHeader, { PINSecurity }, Button, inlineMessages] = useServices([ TOKENS.SCREEN_PIN_EXPLAINER, TOKENS.COMPONENT_PIN_CREATE_HEADER, TOKENS.CONFIG, TOKENS.COMP_BUTTON, + TOKENS.INLINE_ERRORS, ]) const [PINOneValidations, setPINOneValidations] = useState( @@ -93,69 +97,137 @@ const PINCreate: React.FC = ({ setAuthenticated, explainedStatus controlsContainer: {}, }) - const passcodeCreate = useCallback(async (PIN: string) => { - try { - setContinueEnabled(false) - await setWalletPIN(PIN) - // This will trigger initAgent - setAuthenticated(true) + const passcodeCreate = useCallback( + async (PIN: string) => { + try { + setContinueEnabled(false) + await setWalletPIN(PIN) + // This will trigger initAgent + setAuthenticated(true) - dispatch({ - type: DispatchAction.DID_CREATE_PIN, - }) - navigation.dispatch( - CommonActions.reset({ - index: 0, - routes: [{ name: Screens.UseBiometry }], + dispatch({ + type: DispatchAction.DID_CREATE_PIN, }) - ) - } catch (err: unknown) { - const error = new BifoldError(t('Error.Title1040'), t('Error.Message1040'), (err as Error)?.message ?? err, 1040) - DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) - } - }, [setWalletPIN, setAuthenticated, dispatch, navigation, t]) + navigation.dispatch( + CommonActions.reset({ + index: 0, + routes: [{ name: Screens.UseBiometry }], + }) + ) + } catch (err: unknown) { + const error = new BifoldError( + t('Error.Title1040'), + t('Error.Message1040'), + (err as Error)?.message ?? err, + 1040 + ) + DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) + } + }, + [setWalletPIN, setAuthenticated, dispatch, navigation, t] + ) - const validatePINEntry = useCallback((PINOne: string, PINTwo: string): boolean => { - for (const validation of PINOneValidations) { - if (validation.isInvalid) { - setModalState({ - visible: true, - title: t('PINCreate.InvalidPIN'), - message: t(`PINCreate.Message.${validation.errorName}`), - }) + const displayModalMessage = (title: string, message: string) => { + setModalState({ + visible: true, + title: title, + message: message, + }) + } + + const attentionMessage = useCallback( + (title: string, message: string, pinOne: boolean) => { + if (inlineMessages.enabled) { + const config = { + message: message, + inlineType: InlineErrorType.error, + config: inlineMessages, + } + if (pinOne) { + setInlineMessageField1(config) + } else { + setInlineMessageField2(config) + } + } else { + displayModalMessage(title, message) + } + }, + [inlineMessages] + ) + + const validatePINEntry = useCallback( + (PINOne: string, PINTwo: string): boolean => { + for (const validation of PINOneValidations) { + if (validation.isInvalid) { + attentionMessage(t('PINCreate.InvalidPIN'), t(`PINCreate.Message.${validation.errorName}`), true) + return false + } + } + if (PINOne !== PINTwo) { + attentionMessage(t('PINCreate.InvalidPIN'), t('PINCreate.PINsDoNotMatch'), false) return false } - } - if (PINOne !== PINTwo) { - setModalState({ - visible: true, - title: t('PINCreate.InvalidPIN'), - message: t('PINCreate.PINsDoNotMatch'), - }) - return false - } - return true - }, [PINOneValidations, t]) + return true + }, + [PINOneValidations, t, attentionMessage] + ) - const checkOldPIN = useCallback(async (PIN: string): Promise => { - const valid = await checkPIN(PIN) - if (!valid) { - setModalState({ - visible: true, - title: t('PINCreate.InvalidPIN'), - message: t(`PINCreate.Message.OldPINIncorrect`), - }) - } - return valid - }, [checkPIN, t]) + const checkOldPIN = useCallback( + async (PIN: string): Promise => { + const valid = await checkPIN(PIN) + if (!valid) { + displayModalMessage(t('PINCreate.InvalidPIN'), t(`PINCreate.Message.OldPINIncorrect`)) + } + return valid + }, + [checkPIN, t] + ) + + const confirmEntry = useCallback( + async (PINOne: string, PINTwo: string) => { + if (!validatePINEntry(PINOne, PINTwo)) { + return + } - const confirmEntry = useCallback(async (PINOne: string, PINTwo: string) => { - if (!validatePINEntry(PINOne, PINTwo)) { - return + await passcodeCreate(PINOne) + }, + [validatePINEntry, passcodeCreate] + ) + + const handleCreatePinTap = async () => { + setLoading(true) + if (updatePin) { + const valid = validatePINEntry(PIN, PINTwo) + if (valid) { + setContinueEnabled(false) + const oldPinValid = await checkOldPIN(PINOld) + if (oldPinValid) { + const success = await rekeyWallet(PINOld, PIN, store.preferences.useBiometry) + if (success) { + setModalState({ + visible: true, + title: t('PINCreate.PinChangeSuccessTitle'), + message: t('PINCreate.PinChangeSuccessMessage'), + onModalDismiss: () => { + navigation.navigate(Screens.Settings as never) + }, + }) + } + } + setContinueEnabled(true) + } + } else { + await confirmEntry(PIN, PINTwo) } + setLoading(false) + } - await passcodeCreate(PINOne) - }, [validatePINEntry, passcodeCreate]) + const isContinueDisabled = (): boolean => { + if (inlineMessages) { + return false + } + return !continueEnabled || PIN.length < minPINLength || PINTwo.length < minPINLength + } useEffect(() => { if (updatePin) { @@ -164,140 +236,121 @@ const PINCreate: React.FC = ({ setAuthenticated, explainedStatus }, [updatePin, PIN, PINTwo, PINOld]) const continueCreatePIN = () => { - setExplained(true); + setExplained(true) } - return ( - explained ? - ( - - - - {updatePin && ( - { - setPINOld(p) - }} - /> - )} - { - setPIN(p) - setPINOneValidations(PINCreationValidations(p, PINSecurity.rules)) + useEffect(() => { + setInlineMessageField1(undefined) + setInlineMessageField2(undefined) + }, [PIN, PINTwo]) - if (p.length === minPINLength) { - if (PINTwoInputRef && PINTwoInputRef.current) { - PINTwoInputRef.current.focus() - // NOTE:(jl) `findNodeHandle` will be deprecated in React 18. - // https://reactnative.dev/docs/new-architecture-library-intro#preparing-your-javascript-codebase-for-the-new-react-native-renderer-fabric - const reactTag = findNodeHandle(PINTwoInputRef.current) - if (reactTag) { - AccessibilityInfo.setAccessibilityFocus(reactTag) - } - } - } - }} - testID={testIdWithKey('EnterPIN')} - accessibilityLabel={t('PINCreate.EnterPIN')} - autoFocus={false} - /> - {PINSecurity.displayHelper && ( - - {PINOneValidations.map((validation, index) => { - return ( - - {validation.isInvalid ? ( - - ) : ( - - )} - - {t(`PINCreate.Helper.${validation.errorName}`)} - - - ) - })} - - )} + return explained ? ( + + + + + {updatePin && ( { - setPINTwo(p) - if (p.length === minPINLength) { - Keyboard.dismiss() - if (createPINButtonRef && createPINButtonRef.current) { - // NOTE:(jl) `findNodeHandle` will be deprecated in React 18. - // https://reactnative.dev/docs/new-architecture-library-intro#preparing-your-javascript-codebase-for-the-new-react-native-renderer-fabric - const reactTag = findNodeHandle(createPINButtonRef.current) - if (reactTag) { - AccessibilityInfo.setAccessibilityFocus(reactTag) - } - } - } + setPINOld(p) }} - testID={testIdWithKey('ReenterPIN')} - accessibilityLabel={t('PINCreate.ReenterPIN', { new: updatePin ? t('PINCreate.NewPIN') + ' ' : '' })} - autoFocus={false} - ref={PINTwoInputRef} /> - {modalState.visible && ( - { - if (modalState.onModalDismiss) { - modalState.onModalDismiss() + )} + { + setPIN(p) + setPINOneValidations(PINCreationValidations(p, PINSecurity.rules)) + + if (p.length === minPINLength) { + if (PINTwoInputRef && PINTwoInputRef.current) { + PINTwoInputRef.current.focus() + // NOTE:(jl) `findNodeHandle` will be deprecated in React 18. + // https://reactnative.dev/docs/new-architecture-library-intro#preparing-your-javascript-codebase-for-the-new-react-native-renderer-fabric + const reactTag = findNodeHandle(PINTwoInputRef.current) + if (reactTag) { + AccessibilityInfo.setAccessibilityFocus(reactTag) } - setModalState({ ...modalState, visible: false, onModalDismiss: undefined }) - }} - /> - )} - - - - + /> + )} + + + - ) - : () + + + ) : ( + ) } diff --git a/packages/legacy/core/App/screens/PINEnter.tsx b/packages/legacy/core/App/screens/PINEnter.tsx index 581d08a695..7108fffee5 100644 --- a/packages/legacy/core/App/screens/PINEnter.tsx +++ b/packages/legacy/core/App/screens/PINEnter.tsx @@ -25,6 +25,7 @@ import { BifoldError } from '../types/error' import { Screens } from '../types/navigators' import { hashPIN } from '../utils/crypto' import { testIdWithKey } from '../utils/testable' +import { InlineErrorType, InlineMessageProps } from '../components/inputs/InlineErrorText' interface PINEnterProps { setAuthenticated: (status: boolean) => void @@ -50,6 +51,8 @@ const PINEnter: React.FC = ({ setAuthenticated, usage = PINEntryU const { ColorPallet, TextTheme, Assets, PINEnterTheme } = useTheme() const { ButtonLoading } = useAnimatedComponents() const [logger] = useServices([TOKENS.UTIL_LOGGER]) + const [inlineMessageField, setInlineMessageField] = useState() + const [inlineMessages] = useServices([TOKENS.INLINE_ERRORS]) const style = StyleSheet.create({ screenContainer: { @@ -109,6 +112,13 @@ const PINEnter: React.FC = ({ setAuthenticated, usage = PINEntryU } }, [store.onboarding.postAuthScreens, navigation]) + const isContinueDisabled = (): boolean => { + if (inlineMessages.enabled) { + return false + } + return !continueEnabled || PIN.length < minPINLength + } + // listen for biometrics error event useEffect(() => { const handle = DeviceEventEmitter.addListener(EventTypes.BIOMETRY_ERROR, (value?: boolean) => { @@ -229,6 +239,10 @@ const PINEnter: React.FC = ({ setAuthenticated, usage = PINEntryU setDisplayLockoutWarning(displayWarning) }, [store.loginAttempt.loginAttempts, getLockoutPenalty, store.loginAttempt.servedPenalty, attemptLockout]) + useEffect(() => { + setInlineMessageField(undefined) + }, [PIN]) + const unlockWalletWithPIN = useCallback( async (PIN: string) => { try { @@ -343,6 +357,15 @@ const PINEnter: React.FC = ({ setAuthenticated, usage = PINEntryU // both of the async functions called in this function are completely wrapped in trycatch const onPINInputCompleted = useCallback( async (PIN: string) => { + if (inlineMessages.enabled && PIN.length < minPINLength) { + setInlineMessageField({ + message: t('PINCreate.PINTooShort'), + inlineType: InlineErrorType.error, + config: inlineMessages, + }) + return + } + setContinueEnabled(false) if (usage === PINEntryUsage.PINCheck) { @@ -353,7 +376,7 @@ const PINEnter: React.FC = ({ setAuthenticated, usage = PINEntryU await unlockWalletWithPIN(PIN) } }, - [usage, verifyPIN, unlockWalletWithPIN] + [usage, verifyPIN, unlockWalletWithPIN, t, inlineMessages] ) const displayHelpText = useCallback(() => { @@ -407,7 +430,6 @@ const PINEnter: React.FC = ({ setAuthenticated, usage = PINEntryU - {/* */} {displayHelpText()} {t('PINEnter.EnterPIN')} = ({ setAuthenticated, usage = PINEntryU testID={testIdWithKey('EnterPIN')} accessibilityLabel={t('PINEnter.EnterPIN')} autoFocus={true} + inlineMessage={inlineMessageField} /> @@ -428,7 +451,7 @@ const PINEnter: React.FC = ({ setAuthenticated, usage = PINEntryU title={t('PINEnter.Unlock')} buttonType={ButtonType.Primary} testID={testIdWithKey('Enter')} - disabled={!continueEnabled || PIN.length < minPINLength} + disabled={isContinueDisabled()} accessibilityLabel={t('PINEnter.Unlock')} onPress={() => { Keyboard.dismiss() diff --git a/packages/legacy/core/App/theme.ts b/packages/legacy/core/App/theme.ts index a8335d02a0..b32cf5a70e 100644 --- a/packages/legacy/core/App/theme.ts +++ b/packages/legacy/core/App/theme.ts @@ -43,6 +43,8 @@ import HistoryCardRevokedIcon from './assets/img/HistoryCardRevokedIcon.svg' import HistoryInformationSentIcon from './assets/img/HistoryInformationSentIcon.svg' import HistoryPinUpdatedIcon from './assets/img/HistoryPinUpdatedIcon.svg' import IconChevronRight from './assets/img/IconChevronRight.svg' +import IconWarning from './assets/img/exclamation-mark.svg' +import IconError from './assets/img/error-filled.svg' export interface ISVGAssets { activityIndicator: React.FC @@ -87,6 +89,8 @@ export interface ISVGAssets { iconChevronRight: React.FC iconDelete: React.FC iconEdit: React.FC + iconWarning: React.FC + iconError: React.FC } export interface IFontAttributes { @@ -95,6 +99,7 @@ export interface IFontAttributes { fontSize: number fontWeight?: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' color: string + lineHeight?: number } export interface IInputAttributes { @@ -107,6 +112,13 @@ export interface IInputAttributes { borderColor?: string } +export interface IInlineInputMessage { + inlineErrorText: IFontAttributes + InlineErrorIcon: React.FC + inlineWarningText: IFontAttributes + InlineWarningIcon: React.FC +} + export interface IInputs { label: IFontAttributes textInput: IInputAttributes @@ -138,6 +150,8 @@ export interface ITextTheme { modalHeadingOne: IFontAttributes modalHeadingThree: IFontAttributes settingsText: IFontAttributes + inlineErrorText: IFontAttributes + inlineWarningText: IFontAttributes } export interface IBrandColors { @@ -163,6 +177,8 @@ export interface IBrandColors { tabBarInactive: string unorderedList: string unorderedListModal: string + inlineError: string + inlineWarning: string } export interface ISemanticColors { @@ -200,6 +216,11 @@ export interface IGrayscaleColors { white: string } +export interface IErrorColors { + error: string + warning: string +} + export interface IColorPallet { brand: IBrandColors semantic: ISemanticColors @@ -231,6 +252,11 @@ const GrayscaleColors: IGrayscaleColors = { white: '#FFFFFF', } +const InlineErrorMessageColors: IErrorColors = { + error: '#ff0000', + warning: '#ff9000', +} + const BrandColors: IBrandColors = { primary: '#42803E', primaryDisabled: `rgba(53, 130, 63, ${lightOpacity})`, @@ -254,6 +280,8 @@ const BrandColors: IBrandColors = { headerText: GrayscaleColors.white, buttonText: GrayscaleColors.white, tabBarInactive: GrayscaleColors.white, + inlineError: InlineErrorMessageColors.error, + inlineWarning: InlineErrorMessageColors.warning, } const SemanticColors: ISemanticColors = { @@ -386,6 +414,16 @@ export const TextTheme: ITextTheme = { fontWeight: 'normal', color: ColorPallet.brand.text, }, + inlineErrorText: { + fontSize: 16, + fontWeight: 'normal', + color: ColorPallet.brand.inlineError, + }, + inlineWarningText: { + fontSize: 16, + fontWeight: 'normal', + color: ColorPallet.brand.inlineWarning, + }, } export const Inputs: IInputs = StyleSheet.create({ @@ -918,6 +956,8 @@ export const Assets = { iconChevronRight: IconChevronRight, iconDelete: IconDelete, iconEdit: IconEdit, + iconError: IconError, + iconWarning: IconWarning, }, img: { logoPrimary: { @@ -937,9 +977,17 @@ export const Assets = { }, } +const InputInlineMessage: IInlineInputMessage = { + inlineErrorText: { ...TextTheme.inlineErrorText }, + InlineErrorIcon: Assets.svg.iconError, + inlineWarningText: { ...TextTheme.inlineWarningText }, + InlineWarningIcon: Assets.svg.iconWarning, +} + export interface ITheme { ColorPallet: IColorPallet TextTheme: ITextTheme + InputInlineMessage: IInlineInputMessage Inputs: IInputs Buttons: any ListItems: any @@ -962,6 +1010,7 @@ export interface ITheme { export const theme: ITheme = { ColorPallet, TextTheme, + InputInlineMessage, Inputs, Buttons, ListItems, diff --git a/packages/legacy/core/App/types/error.ts b/packages/legacy/core/App/types/error.ts index 946abc7780..b66c755997 100644 --- a/packages/legacy/core/App/types/error.ts +++ b/packages/legacy/core/App/types/error.ts @@ -1,3 +1,5 @@ +import { ViewStyle } from 'react-native' + export class QrCodeScanError extends Error { public data?: string public details?: string @@ -24,3 +26,14 @@ export class BifoldError extends Error { Object.setPrototypeOf(this, BifoldError.prototype) } } + +export type InlineErrorConfig = { + enabled: boolean + position?: InlineErrorPosition + style?: ViewStyle +} + +export enum InlineErrorPosition { + Above, + Below, +} diff --git a/packages/legacy/core/__tests__/screens/__snapshots__/PINEnter.test.tsx.snap b/packages/legacy/core/__tests__/screens/__snapshots__/PINEnter.test.tsx.snap index 1b14edd9d2..fe8df42c73 100644 --- a/packages/legacy/core/__tests__/screens/__snapshots__/PINEnter.test.tsx.snap +++ b/packages/legacy/core/__tests__/screens/__snapshots__/PINEnter.test.tsx.snap @@ -390,7 +390,7 @@ exports[`PINEnter Screen PIN Enter renders correctly 1`] = ` Object { "busy": undefined, "checked": undefined, - "disabled": true, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -415,7 +415,7 @@ exports[`PINEnter Screen PIN Enter renders correctly 1`] = ` onStartShouldSetResponder={[Function]} style={ Object { - "backgroundColor": "rgba(53, 130, 63, 0.35)", + "backgroundColor": "#42803E", "borderRadius": 4, "opacity": 1, "padding": 16, @@ -441,12 +441,7 @@ exports[`PINEnter Screen PIN Enter renders correctly 1`] = ` "fontWeight": "bold", "textAlign": "center", }, - Object { - "color": "#FFFFFF", - "fontSize": 18, - "fontWeight": "bold", - "textAlign": "center", - }, + false, false, false, ] @@ -859,7 +854,7 @@ exports[`PINEnter Screen PIN Enter renders correctly when logged out message is Object { "busy": undefined, "checked": undefined, - "disabled": true, + "disabled": false, "expanded": undefined, "selected": undefined, } @@ -884,7 +879,7 @@ exports[`PINEnter Screen PIN Enter renders correctly when logged out message is onStartShouldSetResponder={[Function]} style={ Object { - "backgroundColor": "rgba(53, 130, 63, 0.35)", + "backgroundColor": "#42803E", "borderRadius": 4, "opacity": 1, "padding": 16, @@ -910,12 +905,7 @@ exports[`PINEnter Screen PIN Enter renders correctly when logged out message is "fontWeight": "bold", "textAlign": "center", }, - Object { - "color": "#FFFFFF", - "fontSize": 18, - "fontWeight": "bold", - "textAlign": "center", - }, + false, false, false, ]