diff --git a/components/FeeBreakdown.tsx b/components/FeeBreakdown.tsx index 92613c21f..6fc5d7327 100644 --- a/components/FeeBreakdown.tsx +++ b/components/FeeBreakdown.tsx @@ -431,7 +431,7 @@ export default class FeeBreakdown extends React.Component< value={`${csv_delay} ${localeString( 'general.blocks' )}`} - infoText={[ + infoModalText={[ localeString( 'views.Channel.csvDelay.info1' ), diff --git a/components/KeyValue.tsx b/components/KeyValue.tsx index 3c936f564..20f66922e 100644 --- a/components/KeyValue.tsx +++ b/components/KeyValue.tsx @@ -24,9 +24,12 @@ interface KeyValueProps { color?: string; indicatorColor?: string; sensitive?: boolean; - infoText?: string | Array; - infoLink?: string; - infoNav?: string; + infoModalText?: string | Array; + infoModalLink?: string; + infoModalAdditionalButtons?: Array<{ + title: string; + callback?: () => void; + }>; mempoolLink?: () => void; disableCopy?: boolean; ModalStore?: ModalStore; @@ -43,9 +46,9 @@ export default class KeyValue extends React.Component { color, indicatorColor, sensitive, - infoText, - infoLink, - infoNav, + infoModalText, + infoModalLink, + infoModalAdditionalButtons, mempoolLink, disableCopy, ModalStore, @@ -86,7 +89,7 @@ export default class KeyValue extends React.Component { > {keyValue} - {infoText && ( + {infoModalText && ( { ); let Key: any; - if (infoText) { + if (infoModalText) { Key = ( toggleInfoModal(infoText, infoLink, infoNav)} + onPress={() => + toggleInfoModal( + infoModalText, + infoModalLink, + infoModalAdditionalButtons + ) + } > {KeyBase} diff --git a/components/Modals/AlertModal.tsx b/components/Modals/AlertModal.tsx index 7bf8a9dbf..9e74a1d37 100644 --- a/components/Modals/AlertModal.tsx +++ b/components/Modals/AlertModal.tsx @@ -110,12 +110,7 @@ export default class AlertModal extends React.Component { )} - + + /> )} @@ -170,12 +165,7 @@ export default class AlertModal extends React.Component { {peers.join(', ')} - + + /> )} @@ -196,7 +186,7 @@ export default class AlertModal extends React.Component { title={localeString('general.close')} onPress={() => toggleAlertModal(false)} secondary - > + /> @@ -216,11 +206,9 @@ const styles = StyleSheet.create({ fontSize: 20, marginBottom: 10 }, - buttons: { - width: '100%' - }, button: { - marginTop: 20, - width: 350 + width: '100%', + alignItems: 'center', + marginVertical: 20 } }); diff --git a/components/Modals/InfoModal.tsx b/components/Modals/InfoModal.tsx index 6b969fa28..ef9fa9e5a 100644 --- a/components/Modals/InfoModal.tsx +++ b/components/Modals/InfoModal.tsx @@ -2,8 +2,6 @@ import React from 'react'; import { View, StyleSheet, Text } from 'react-native'; import { inject, observer } from 'mobx-react'; -import NavigationService from '../../NavigationService'; - import Button from '../Button'; import ModalBox from '../ModalBox'; @@ -26,7 +24,7 @@ export default class InfoModal extends React.Component { showInfoModal, infoModalText, infoModalLink, - infoModalNav, + infoModalAdditionalButtons, toggleInfoModal } = ModalStore; @@ -88,7 +86,27 @@ export default class InfoModal extends React.Component { ))} - {(infoModalLink || infoModalNav) && ( + {infoModalAdditionalButtons?.map( + ({ title, callback }, index) => ( + + + /> )} @@ -117,7 +130,7 @@ export default class InfoModal extends React.Component { title={localeString('general.close')} onPress={() => toggleInfoModal()} secondary - > + /> @@ -129,7 +142,8 @@ export default class InfoModal extends React.Component { const styles = StyleSheet.create({ buttons: { - width: '100%' + width: '100%', + alignItems: 'center' }, button: { width: 350 diff --git a/components/Text.tsx b/components/Text.tsx index 714eeee34..53168b310 100644 --- a/components/Text.tsx +++ b/components/Text.tsx @@ -11,17 +11,26 @@ interface TextProps { ModalStore?: ModalStore; style?: TextStyle; children?: string; - infoText?: string | Array; - infoLink?: string; - infoNav?: string; + infoModalText?: string | Array; + infoModalLink?: string; + infoModalAdditionalButtons?: Array<{ + title: string; + callback?: () => void; + }>; } @inject('ModalStore') @observer export default class ZeusText extends React.Component { render() { - const { children, style, infoText, infoLink, infoNav, ModalStore } = - this.props; + const { + children, + style, + infoModalText, + infoModalLink, + infoModalAdditionalButtons, + ModalStore + } = this.props; const { toggleInfoModal } = ModalStore!; const CoreText = () => ( @@ -35,7 +44,7 @@ export default class ZeusText extends React.Component { > {children} - {infoText && ( + {infoModalText && ( { ); - if (infoText) { + if (infoModalText) { return ( toggleInfoModal(infoText, infoLink, infoNav)} + onPress={() => + toggleInfoModal( + infoModalText, + infoModalLink, + infoModalAdditionalButtons + ) + } > diff --git a/locales/en.json b/locales/en.json index f47e0ac48..219647e56 100644 --- a/locales/en.json +++ b/locales/en.json @@ -684,6 +684,7 @@ "views.Settings.enabled": "Enabled", "views.Settings.disabled": "Disabled", "views.Settings.newPassword": "New Password", + "views.Settings.createYourPassword": "Create your Password", "views.Settings.confirmPassword": "Confirm New Password", "views.Settings.newDuressPassword": "New Duress Password", "views.Settings.confirmDuressPassword": "Confirm Duress Password", @@ -777,10 +778,12 @@ "views.Settings.SetDuressPassword.deletePassword": "Delete Duress Password", "views.Settings.SetDuressPassword.duressPasswordExplanation": "Once set, you can enter your duress password on the login screen to delete all of your wallet configurations.", "views.Settings.SetPin.title": "Set / Change PIN", + "views.Settings.Security.BiometryRequiresPinOrPassword": "To enable biometric authentication, you need to set up a PIN or Password first as a backup method.", "views.Settings.Security.FaceID.title": "FaceID", "views.Settings.Security.TouchID.title": "TouchID", "views.Settings.Security.Biometrics.title": "Biometrics", "views.Settings.Security.Biometrics.prompt": "Unlock", + "views.Settings.Security.biometricsWillBeDisabled": "Deleting your PIN or Password will also disable biometric authentication. Continue?", "views.Lockscreen.Biometrics.prompt": "Unlock Zeus", "views.Settings.SetPin.noMatch": "PINs do not match. Please resubmit.", "views.Settings.SetPin.invalid": "PIN and Duress PIN cannot be equal.", diff --git a/stores/ModalStore.ts b/stores/ModalStore.ts index 4d9e09b04..1b7a4b5d1 100644 --- a/stores/ModalStore.ts +++ b/stores/ModalStore.ts @@ -9,7 +9,10 @@ export default class ModalStore { @observable public clipboardValue: string; @observable public infoModalText: string | Array | undefined; @observable public infoModalLink: string | undefined; - @observable public infoModalNav: string | undefined; + @observable public infoModalAdditionalButtons?: Array<{ + title: string; + callback?: () => void; + }>; @observable public alertModalText: string | Array | undefined; @observable public alertModalLink: string | undefined; @observable public alertModalNav: string | undefined; @@ -25,12 +28,12 @@ export default class ModalStore { public toggleInfoModal = ( text?: string | Array, link?: string, - nav?: string + buttons?: Array<{ title: string; callback?: () => void }> ) => { this.showInfoModal = text ? true : false; this.infoModalText = text; this.infoModalLink = link; - this.infoModalNav = nav; + this.infoModalAdditionalButtons = buttons; }; @action @@ -73,7 +76,7 @@ export default class ModalStore { this.showInfoModal = false; this.infoModalText = ''; this.infoModalLink = ''; - this.infoModalNav = ''; + this.infoModalAdditionalButtons = undefined; return true; } return false; diff --git a/views/Channels/Channel.tsx b/views/Channels/Channel.tsx index 252d34c6c..1c16ed304 100644 --- a/views/Channels/Channel.tsx +++ b/views/Channels/Channel.tsx @@ -666,10 +666,10 @@ export default class ChannelView extends React.Component< } /> } - infoText={localeString( + infoModalText={localeString( 'views.Channel.localReserve.info' )} - infoLink="https://bitcoin.design/guide/how-it-works/liquidity/#what-is-a-channel-reserve" + infoModalLink="https://bitcoin.design/guide/how-it-works/liquidity/#what-is-a-channel-reserve" indicatorColor={themeColor('outboundReserve')} /> )} @@ -685,10 +685,10 @@ export default class ChannelView extends React.Component< toggleable /> } - infoText={localeString( + infoModalText={localeString( 'views.Channel.remoteReserve.info' )} - infoLink="https://bitcoin.design/guide/how-it-works/liquidity/#what-is-a-channel-reserve" + infoModalLink="https://bitcoin.design/guide/how-it-works/liquidity/#what-is-a-channel-reserve" indicatorColor={themeColor('inboundReserve')} /> )} @@ -824,7 +824,7 @@ export default class ChannelView extends React.Component< ...styles.text, color: themeColor('text') }} - infoText={localeString( + infoModalText={localeString( 'views.Channel.externalAddress.info' )} > diff --git a/views/Lockscreen.tsx b/views/Lockscreen.tsx index 3ff001f83..333b8c861 100644 --- a/views/Lockscreen.tsx +++ b/views/Lockscreen.tsx @@ -313,12 +313,14 @@ export default class Lockscreen extends React.Component< const { updateSettings } = SettingsStore; // duress pin is also deleted when pin is deleted + // biometry is also disabled when pin is deleted updateSettings({ pin: '', duressPin: '', - authenticationAttempts: 0 + authenticationAttempts: 0, + isBiometryEnabled: false }).then(() => { - navigation.pop(2); + navigation.popTo('Security'); }); }; @@ -330,7 +332,7 @@ export default class Lockscreen extends React.Component< duressPin: '', authenticationAttempts: 0 }).then(() => { - navigation.pop(2); + navigation.popTo('Security'); }); }; diff --git a/views/Receive.tsx b/views/Receive.tsx index 28a2b5385..2c2430aa4 100644 --- a/views/Receive.tsx +++ b/views/Receive.tsx @@ -1984,7 +1984,7 @@ export default class Receive extends React.Component< ), top: 20 }} - infoText={[ + infoModalText={[ localeString( 'views.Receive.lspSwitchExplainer1' ), @@ -1992,7 +1992,17 @@ export default class Receive extends React.Component< 'views.Receive.lspSwitchExplainer2' ) ]} - infoNav="LspExplanationOverview" + infoModalAdditionalButtons={[ + { + title: localeString( + 'general.learnMore' + ), + callback: () => + navigation.navigate( + 'LspExplanationOverview' + ) + } + ]} > {localeString( 'views.Settings.LSP.enableLSP' @@ -2488,7 +2498,7 @@ export default class Receive extends React.Component< ), top: 20 }} - infoText={[ + infoModalText={[ localeString( 'views.Receive.routeHintSwitchExplainer1' ), @@ -2622,7 +2632,7 @@ export default class Receive extends React.Component< ), top: 20 }} - infoText={[ + infoModalText={[ localeString( 'views.Receive.ampSwitchExplainer1' ), @@ -2630,7 +2640,7 @@ export default class Receive extends React.Component< 'views.Receive.ampSwitchExplainer2' ) ]} - infoLink="https://docs.lightning.engineering/lightning-network-tools/lnd/amp" + infoModalLink="https://docs.lightning.engineering/lightning-network-tools/lnd/amp" > {localeString( 'views.Receive.ampInvoice' @@ -2660,7 +2670,7 @@ export default class Receive extends React.Component< ), top: 20 }} - infoText={[ + infoModalText={[ localeString( 'views.Receive.blindedPathsExplainer1' ), @@ -2668,7 +2678,7 @@ export default class Receive extends React.Component< 'views.Receive.blindedPathsExplainer2' ) ]} - infoLink="https://lightningprivacy.com/en/blinded-trampoline" + infoModalLink="https://lightningprivacy.com/en/blinded-trampoline" > {localeString( 'views.Receive.blindedPaths' diff --git a/views/Settings/InvoicesSettings.tsx b/views/Settings/InvoicesSettings.tsx index 5ad4cad22..80f962ff9 100644 --- a/views/Settings/InvoicesSettings.tsx +++ b/views/Settings/InvoicesSettings.tsx @@ -342,7 +342,7 @@ export default class InvoicesSettings extends React.Component< color: themeColor('secondaryText'), top: 20 }} - infoText={[ + infoModalText={[ localeString( 'views.Receive.routeHintSwitchExplainer1' ), @@ -386,7 +386,7 @@ export default class InvoicesSettings extends React.Component< color: themeColor('secondaryText'), top: 20 }} - infoText={[ + infoModalText={[ localeString( 'views.Receive.ampSwitchExplainer1' ), @@ -394,6 +394,7 @@ export default class InvoicesSettings extends React.Component< 'views.Receive.ampSwitchExplainer2' ) ]} + infoModalLink="https://docs.lightning.engineering/lightning-network-tools/lnd/amp" > {localeString('views.Receive.ampInvoice')} @@ -429,7 +430,7 @@ export default class InvoicesSettings extends React.Component< color: themeColor('secondaryText'), top: 20 }} - infoText={[ + infoModalText={[ localeString( 'views.Receive.blindedPathsExplainer1' ), diff --git a/views/Settings/LightningAddress/LightningAddressSettings.tsx b/views/Settings/LightningAddress/LightningAddressSettings.tsx index 3c90eb947..3de878655 100644 --- a/views/Settings/LightningAddress/LightningAddressSettings.tsx +++ b/views/Settings/LightningAddress/LightningAddressSettings.tsx @@ -206,7 +206,7 @@ export default class LightningAddressSettings extends React.Component< fontFamily: 'PPNeueMontreal-Book', fontSize: 17 }} - infoText={[ + infoModalText={[ localeString( 'views.Settings.LightningAddressSettings.routeHintsExplainer1' ), diff --git a/views/Settings/LightningAddress/index.tsx b/views/Settings/LightningAddress/index.tsx index c8677a6a7..9b8d9f5dc 100644 --- a/views/Settings/LightningAddress/index.tsx +++ b/views/Settings/LightningAddress/index.tsx @@ -343,7 +343,7 @@ export default class LightningAddress extends React.Component< ), left: 5 }} - infoText={[ + infoModalText={[ localeString( 'views.Settings.LightningAddress.statusExplainer1' ), diff --git a/views/Settings/Privacy.tsx b/views/Settings/Privacy.tsx index 067dedc74..416086792 100644 --- a/views/Settings/Privacy.tsx +++ b/views/Settings/Privacy.tsx @@ -173,7 +173,7 @@ export default class Privacy extends React.Component< fontFamily: 'PPNeueMontreal-Book', left: -10 }} - infoText={localeString( + infoModalText={localeString( 'views.Settings.Privacy.clipboard.explainer' ).replace('Zeus', 'ZEUS')} > @@ -218,7 +218,7 @@ export default class Privacy extends React.Component< fontFamily: 'PPNeueMontreal-Book', left: -10 }} - infoText={[ + infoModalText={[ localeString( 'views.Settings.Privacy.lurkerMode.explainer1' ), diff --git a/views/Settings/Security.tsx b/views/Settings/Security.tsx index 4bed23027..410277374 100644 --- a/views/Settings/Security.tsx +++ b/views/Settings/Security.tsx @@ -10,6 +10,7 @@ import Screen from '../../components/Screen'; import Switch from '../../components/Switch'; import SettingsStore from '../../stores/SettingsStore'; +import ModalStore from '../../stores/ModalStore'; import { verifyBiometry } from '../../utils/BiometricUtils'; import { localeString } from '../../utils/LocaleUtils'; @@ -18,6 +19,7 @@ import { themeColor } from '../../utils/ThemeUtils'; interface SecurityProps { navigation: StackNavigationProp; SettingsStore: SettingsStore; + ModalStore: ModalStore; } interface SecurityState { @@ -28,6 +30,7 @@ interface SecurityState { passphraseExists: boolean; supportedBiometryType: BiometryType | undefined; isBiometryEnabled: boolean | undefined; + pendingBiometricsEnable: boolean; } const possibleSecurityItems = [ @@ -57,7 +60,7 @@ const possibleSecurityItems = [ } ]; -@inject('SettingsStore') +@inject('SettingsStore', 'ModalStore') @observer export default class Security extends React.Component< SecurityProps, @@ -65,9 +68,6 @@ export default class Security extends React.Component< > { constructor(props: SecurityProps) { super(props); - - this.handleBiometricsSwitchChange = - this.handleBiometricsSwitchChange.bind(this); } state = { @@ -77,7 +77,8 @@ export default class Security extends React.Component< pinExists: false, passphraseExists: false, supportedBiometryType: undefined, - isBiometryEnabled: undefined + isBiometryEnabled: undefined, + pendingBiometricsEnable: false }; componentDidMount() { @@ -145,25 +146,73 @@ export default class Security extends React.Component< displaySecurityItems: minPinItems }); } + + // If user tried to enable biometrics, but was forced to first set up pin or password, + // call handleBiometricsSwitchChange again + if ( + this.state.pendingBiometricsEnable && + (settings.pin || settings.passphrase) + ) { + this.handleBiometricsSwitchChange(true); + } }; async handleBiometricsSwitchChange(value: boolean): Promise { - const isVerified = await verifyBiometry( - localeString(`views.Settings.Security.Biometrics.prompt`) - ); + const { SettingsStore, ModalStore, navigation } = this.props; - if (isVerified) { - const { - SettingsStore: { updateSettings } - } = this.props; + if (value) { + const settings = SettingsStore.settings; + if (!settings.pin && !settings.passphrase) { + this.setState({ pendingBiometricsEnable: true }); + ModalStore.toggleInfoModal( + localeString( + 'views.Settings.Security.BiometryRequiresPinOrPassword' + ), + undefined, + [ + { + title: localeString( + 'views.Settings.createYourPassword' + ), + callback: () => navigation.navigate('SetPassword') + }, + { + title: localeString('views.Settings.newPin'), + callback: () => navigation.navigate('SetPin') + } + ] + ); + return; + } - this.setState({ - isBiometryEnabled: value - }); + const isVerified = await verifyBiometry( + localeString('views.Settings.Security.Biometrics.prompt') + ); - updateSettings({ - isBiometryEnabled: value - }); + if (isVerified) { + this.setState({ + isBiometryEnabled: value, + pendingBiometricsEnable: false + }); + + SettingsStore.updateSettings({ + isBiometryEnabled: value + }); + } + } else { + const isVerified = await verifyBiometry( + localeString(`views.Settings.Security.Biometrics.prompt`) + ); + + if (isVerified) { + this.setState({ + isBiometryEnabled: value + }); + + SettingsStore.updateSettings({ + isBiometryEnabled: value + }); + } } } @@ -177,11 +226,28 @@ export default class Security extends React.Component< ); navigateSecurity = (item: any) => { - const { navigation, SettingsStore } = this.props; + const { navigation, SettingsStore, ModalStore } = this.props; const { settings }: any = SettingsStore; + const { isBiometryEnabled } = this.state; if (!(settings.passphrase || settings.pin)) { navigation.navigate(item.screen); + } else if (item.action === 'DeletePin' && isBiometryEnabled) { + ModalStore.toggleInfoModal( + localeString( + 'views.Settings.Security.biometricsWillBeDisabled' + ), + undefined, + [ + { + title: localeString('general.ok'), + callback: () => + navigation.navigate('Lockscreen', { + deletePin: true + }) + } + ] + ); } else if (item.action === 'DeletePin') { navigation.navigate('Lockscreen', { deletePin: true @@ -281,8 +347,8 @@ export default class Security extends React.Component< + this.handleBiometricsSwitchChange(value) } /> diff --git a/views/Settings/SetPassword.tsx b/views/Settings/SetPassword.tsx index cd9a3e5ec..368197d6a 100644 --- a/views/Settings/SetPassword.tsx +++ b/views/Settings/SetPassword.tsx @@ -12,10 +12,12 @@ import TextInput from '../../components/TextInput'; import { localeString } from '../../utils/LocaleUtils'; import { themeColor } from '../../utils/ThemeUtils'; import SettingsStore from '../../stores/SettingsStore'; +import ModalStore from '../../stores/ModalStore'; interface SetPassphraseProps { navigation: StackNavigationProp; SettingsStore: SettingsStore; + ModalStore: ModalStore; } interface SetPassphraseState { @@ -26,9 +28,10 @@ interface SetPassphraseState { passphraseInvalidError: boolean; passphraseEmptyError: boolean; confirmDelete: boolean; + isBiometryEnabled: boolean; } -@inject('SettingsStore') +@inject('SettingsStore', 'ModalStore') @observer export default class SetPassphrase extends React.Component< SetPassphraseProps, @@ -41,14 +44,17 @@ export default class SetPassphrase extends React.Component< passphraseMismatchError: false, passphraseInvalidError: false, passphraseEmptyError: false, - confirmDelete: false + confirmDelete: false, + isBiometryEnabled: false }; async componentDidMount() { const { SettingsStore } = this.props; - const { getSettings } = SettingsStore; - const settings = await getSettings(); + const settings = await SettingsStore.getSettings(); + this.setState({ + isBiometryEnabled: settings.isBiometryEnabled + }); if (settings.passphrase) { this.setState({ savedPassphrase: settings.passphrase }); } @@ -96,9 +102,7 @@ export default class SetPassphrase extends React.Component< await updateSettings({ passphrase }).then(() => { setLoginStatus(true); getSettings(); - navigation.popTo('Settings', { - refresh: true - }); + navigation.popTo('Security', { refresh: true }); }); }; @@ -110,12 +114,10 @@ export default class SetPassphrase extends React.Component< await updateSettings({ duressPassphrase: '', - passphrase: '' - }).then(() => { - navigation.popTo('Settings', { - refresh: true - }); + passphrase: '', + isBiometryEnabled: false }); + navigation.popTo('Security', { refresh: true }); }; render() { @@ -251,6 +253,22 @@ export default class SetPassphrase extends React.Component< this.setState({ confirmDelete: true }); + } else if (this.state.isBiometryEnabled) { + this.props.ModalStore.toggleInfoModal( + localeString( + 'views.Settings.Security.biometricsWillBeDisabled' + ), + undefined, + [ + { + title: localeString( + 'general.ok' + ), + callback: () => + this.deletePassword() + } + ] + ); } else { this.deletePassword(); } diff --git a/views/Settings/SetPin.tsx b/views/Settings/SetPin.tsx index 6aa69fdb1..127ba3c5c 100644 --- a/views/Settings/SetPin.tsx +++ b/views/Settings/SetPin.tsx @@ -95,9 +95,7 @@ export default class SetPin extends React.Component { await updateSettings({ pin }).then(() => { setLoginStatus(true); getSettings(); - navigation.popTo('Settings', { - refresh: true - }); + navigation.popTo('Security'); }); };