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 @@
+
+
+
\ 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,
]