Enhanced InfoModal and Security UX

This commit is contained in:
myxmaster
2024-11-23 14:21:32 +01:00
parent 09136c90ae
commit 0d30745ed0
17 changed files with 244 additions and 113 deletions
+1 -1
View File
@@ -431,7 +431,7 @@ export default class FeeBreakdown extends React.Component<
value={`${csv_delay} ${localeString(
'general.blocks'
)}`}
infoText={[
infoModalText={[
localeString(
'views.Channel.csvDelay.info1'
),
+18 -9
View File
@@ -24,9 +24,12 @@ interface KeyValueProps {
color?: string;
indicatorColor?: string;
sensitive?: boolean;
infoText?: string | Array<string>;
infoLink?: string;
infoNav?: string;
infoModalText?: string | Array<string>;
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<KeyValueProps, {}> {
color,
indicatorColor,
sensitive,
infoText,
infoLink,
infoNav,
infoModalText,
infoModalLink,
infoModalAdditionalButtons,
mempoolLink,
disableCopy,
ModalStore,
@@ -86,7 +89,7 @@ export default class KeyValue extends React.Component<KeyValueProps, {}> {
>
{keyValue}
</Text>
{infoText && (
{infoModalText && (
<Text
style={{
color:
@@ -103,10 +106,16 @@ export default class KeyValue extends React.Component<KeyValueProps, {}> {
);
let Key: any;
if (infoText) {
if (infoModalText) {
Key = (
<TouchableOpacity
onPress={() => toggleInfoModal(infoText, infoLink, infoNav)}
onPress={() =>
toggleInfoModal(
infoModalText,
infoModalLink,
infoModalAdditionalButtons
)
}
>
{KeyBase}
</TouchableOpacity>
+8 -20
View File
@@ -110,12 +110,7 @@ export default class AlertModal extends React.Component<AlertModalProps, {}> {
)}
</Text>
<View
style={{
...styles.button,
marginBottom: 25
}}
>
<View style={styles.button}>
<Button
title={localeString(
'components.AlertModal.resetEGS'
@@ -129,7 +124,7 @@ export default class AlertModal extends React.Component<AlertModalProps, {}> {
restartNeeded();
}}
tertiary
></Button>
/>
</View>
</>
)}
@@ -170,12 +165,7 @@ export default class AlertModal extends React.Component<AlertModalProps, {}> {
{peers.join(', ')}
</Text>
<View
style={{
...styles.button,
marginBottom: 25
}}
>
<View style={styles.button}>
<Button
title={localeString(
'components.AlertModal.reviewPeers'
@@ -187,7 +177,7 @@ export default class AlertModal extends React.Component<AlertModalProps, {}> {
);
}}
tertiary
></Button>
/>
</View>
</>
)}
@@ -196,7 +186,7 @@ export default class AlertModal extends React.Component<AlertModalProps, {}> {
title={localeString('general.close')}
onPress={() => toggleAlertModal(false)}
secondary
></Button>
/>
</View>
</View>
</ModalBox>
@@ -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
}
});
+27 -13
View File
@@ -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<InfoModalProps, {}> {
showInfoModal,
infoModalText,
infoModalLink,
infoModalNav,
infoModalAdditionalButtons,
toggleInfoModal
} = ModalStore;
@@ -88,7 +86,27 @@ export default class InfoModal extends React.Component<InfoModalProps, {}> {
))}
<View style={styles.buttons}>
{(infoModalLink || infoModalNav) && (
{infoModalAdditionalButtons?.map(
({ title, callback }, index) => (
<View
key={index}
style={{
...styles.button,
marginBottom: 25
}}
>
<Button
title={title}
onPress={() => {
toggleInfoModal();
if (callback) callback();
}}
tertiary
/>
</View>
)
)}
{infoModalLink && (
<View
style={{
...styles.button,
@@ -101,15 +119,10 @@ export default class InfoModal extends React.Component<InfoModalProps, {}> {
)}
onPress={() => {
toggleInfoModal();
if (infoModalLink)
UrlUtils.goToUrl(infoModalLink);
if (infoModalNav)
NavigationService.navigate(
infoModalNav
);
UrlUtils.goToUrl(infoModalLink);
}}
tertiary
></Button>
/>
</View>
)}
<View style={styles.button}>
@@ -117,7 +130,7 @@ export default class InfoModal extends React.Component<InfoModalProps, {}> {
title={localeString('general.close')}
onPress={() => toggleInfoModal()}
secondary
></Button>
/>
</View>
</View>
</View>
@@ -129,7 +142,8 @@ export default class InfoModal extends React.Component<InfoModalProps, {}> {
const styles = StyleSheet.create({
buttons: {
width: '100%'
width: '100%',
alignItems: 'center'
},
button: {
width: 350
+23 -8
View File
@@ -11,17 +11,26 @@ interface TextProps {
ModalStore?: ModalStore;
style?: TextStyle;
children?: string;
infoText?: string | Array<string>;
infoLink?: string;
infoNav?: string;
infoModalText?: string | Array<string>;
infoModalLink?: string;
infoModalAdditionalButtons?: Array<{
title: string;
callback?: () => void;
}>;
}
@inject('ModalStore')
@observer
export default class ZeusText extends React.Component<TextProps, {}> {
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<TextProps, {}> {
>
{children}
</Text>
{infoText && (
{infoModalText && (
<Text
style={{
color: themeColor('text'),
@@ -49,10 +58,16 @@ export default class ZeusText extends React.Component<TextProps, {}> {
</Row>
);
if (infoText) {
if (infoModalText) {
return (
<TouchableOpacity
onPress={() => toggleInfoModal(infoText, infoLink, infoNav)}
onPress={() =>
toggleInfoModal(
infoModalText,
infoModalLink,
infoModalAdditionalButtons
)
}
>
<CoreText />
</TouchableOpacity>
+4
View File
@@ -14,6 +14,7 @@
"general.error": "Error",
"general.danger": "Danger!",
"general.ok": "OK",
"general.continueQuestion": "Continue?",
"general.readOnlyWallet": "Read-only wallet",
"general.custodialWallet": "Custodial wallet",
"general.show": "Show",
@@ -684,6 +685,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 +779,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.",
"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.",
+7 -4
View File
@@ -9,7 +9,10 @@ export default class ModalStore {
@observable public clipboardValue: string;
@observable public infoModalText: string | Array<string> | undefined;
@observable public infoModalLink: string | undefined;
@observable public infoModalNav: string | undefined;
@observable public infoModalAdditionalButtons?: Array<{
title: string;
callback?: () => void;
}>;
@observable public alertModalText: string | Array<string> | undefined;
@observable public alertModalLink: string | undefined;
@observable public alertModalNav: string | undefined;
@@ -25,12 +28,12 @@ export default class ModalStore {
public toggleInfoModal = (
text?: string | Array<string>,
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;
+5 -5
View File
@@ -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'
)}
>
+5 -3
View File
@@ -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');
});
};
+17 -7
View File
@@ -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'
+4 -3
View File
@@ -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')}
</Text>
@@ -429,7 +430,7 @@ export default class InvoicesSettings extends React.Component<
color: themeColor('secondaryText'),
top: 20
}}
infoText={[
infoModalText={[
localeString(
'views.Receive.blindedPathsExplainer1'
),
@@ -206,7 +206,7 @@ export default class LightningAddressSettings extends React.Component<
fontFamily: 'PPNeueMontreal-Book',
fontSize: 17
}}
infoText={[
infoModalText={[
localeString(
'views.Settings.LightningAddressSettings.routeHintsExplainer1'
),
+1 -1
View File
@@ -343,7 +343,7 @@ export default class LightningAddress extends React.Component<
),
left: 5
}}
infoText={[
infoModalText={[
localeString(
'views.Settings.LightningAddress.statusExplainer1'
),
+2 -2
View File
@@ -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'
),
+90 -21
View File
@@ -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<any, any>;
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<void> {
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,31 @@ 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'
),
localeString('general.continueQuestion')
],
undefined,
[
{
title: localeString('general.ok'),
callback: () =>
navigation.navigate('Lockscreen', {
deletePin: true
})
}
]
);
} else if (item.action === 'DeletePin') {
navigation.navigate('Lockscreen', {
deletePin: true
@@ -281,8 +350,8 @@ export default class Security extends React.Component<
<Switch
value={isBiometryEnabled}
onValueChange={
this.handleBiometricsSwitchChange
onValueChange={(value: boolean) =>
this.handleBiometricsSwitchChange(value)
}
/>
</ListItem>
+30 -12
View File
@@ -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<any, any>;
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();
}
+1 -3
View File
@@ -95,9 +95,7 @@ export default class SetPin extends React.Component<SetPinProps, SetPinState> {
await updateSettings({ pin }).then(() => {
setLoginStatus(true);
getSettings();
navigation.popTo('Settings', {
refresh: true
});
navigation.popTo('Security');
});
};