ADD: Privacy Settings Screen (#1967)

* ADD: Privacy Settings Screen

* REF: Clipboard read opt-out

* ADD: Wallet shortcut privacy

* FIX: Remove unused code

* REF:Addasync/await

* REF: BlueClipboard

* REF: Rename file
This commit is contained in:
Marcos Rodriguez Vélez 2020-10-10 15:19:42 -04:00 committed by GitHub
parent 04a85f8272
commit 5ece75b691
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 222 additions and 30 deletions

10
App.js
View file

@ -11,7 +11,6 @@ import {
Platform,
View,
} from 'react-native';
import Clipboard from '@react-native-community/clipboard';
import Modal from 'react-native-modal';
import { NavigationContainer, CommonActions } from '@react-navigation/native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
@ -27,8 +26,8 @@ import DeeplinkSchemaMatch from './class/deeplink-schema-match';
import loc from './loc';
import { BlueDefaultTheme, BlueDarkTheme, BlueCurrentTheme } from './components/themes';
import InitRoot from './Navigation';
import BlueClipboard from './blue_modules/clipboard';
const A = require('./blue_modules/analytics');
if (process.env.NODE_ENV !== 'development') {
Sentry.init({
dsn: 'https://23377936131848ca8003448a893cb622@sentry.io/1295736',
@ -188,10 +187,9 @@ export default class App extends React.Component {
if (BlueApp.getWallets().length > 0) {
if ((this.state.appState.match(/background/) && nextAppState) === 'active' || nextAppState === undefined) {
setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000);
const processed = await this._processPushNotifications();
if (processed) return;
const clipboard = await Clipboard.getString();
const clipboard = await BlueClipboard.getClipboardContent();
const isAddressFromStoredWallet = BlueApp.getWallets().some(wallet => {
if (wallet.chain === Chain.ONCHAIN) {
// checking address validity is faster than unwrapping hierarchy only to compare it to garbage
@ -278,10 +276,10 @@ export default class App extends React.Component {
<View style={styles.space} />
<BlueButton
noMinWidth
title="OK"
title={loc._.ok}
onPress={() => {
this.setState({ isClipboardContentModalVisible: false }, async () => {
const clipboard = await Clipboard.getString();
const clipboard = await BlueClipboard.getClipboardContent();
setTimeout(() => this.handleOpenURL({ url: clipboard }), 100);
});
}}

View file

@ -69,7 +69,7 @@ export const BlueButton = props => {
const { colors } = useTheme();
const { width } = useWindowDimensions();
let backgroundColor = props.backgroundColor ? props.backgroundColor : colors.mainColor;
let backgroundColor = props.backgroundColor ? props.backgroundColor : colors.mainColor || BlueCurrentTheme.colors.mainColor;
let fontColor = colors.buttonTextColor;
if (props.disabled === true) {
backgroundColor = colors.buttonDisabledBackgroundColor;
@ -746,9 +746,15 @@ export const BlueListItem = React.memo(props => {
</ListItem.Title>
)}
</ListItem.Content>
{props.chevron && <ListItem.Chevron />}
{props.rightIcon && <Avatar icon={props.rightIcon} />}
{props.switch && <Switch {...props.switch} />}
{props.isLoading ? (
<ActivityIndicator />
) : (
<>
{props.chevron && <ListItem.Chevron />}
{props.rightIcon && <Avatar icon={props.rightIcon} />}
{props.switch && <Switch {...props.switch} />}
</>
)}
</ListItem>
);
});

View file

@ -70,6 +70,7 @@ import UnlockWith from './UnlockWith';
import { BlueNavigationStyle } from './BlueComponents';
import DrawerList from './screen/wallets/drawerList';
import { isTablet } from 'react-native-device-info';
import SettingsPrivacy from './screen/settings/SettingsPrivacy';
const defaultScreenOptions =
Platform.OS === 'ios'
@ -136,6 +137,7 @@ const WalletsRoot = () => (
<WalletsStack.Screen name="PlausibleDeniability" component={PlausibleDeniability} options={PlausibleDeniability.navigationOptions} />
<WalletsStack.Screen name="LightningSettings" component={LightningSettings} options={LightningSettings.navigationOptions} />
<WalletsStack.Screen name="ElectrumSettings" component={ElectrumSettings} options={ElectrumSettings.navigationOptions} />
<WalletsStack.Screen name="SettingsPrivacy" component={SettingsPrivacy} options={SettingsPrivacy.navigationOptions} />
<WalletsStack.Screen
name="LNDViewInvoice"
component={LNDViewInvoice}

38
blue_modules/clipboard.js Normal file
View file

@ -0,0 +1,38 @@
import { useAsyncStorage } from '@react-native-community/async-storage';
import Clipboard from '@react-native-community/clipboard';
function BlueClipboard() {
BlueClipboard.STORAGE_KEY = 'ClipboardReadAllowed';
const isClipboardAccessAllowed = useAsyncStorage(BlueClipboard.STORAGE_KEY).getItem;
const setIsClipboardAccessAllowed = useAsyncStorage(BlueClipboard.STORAGE_KEY).setItem;
BlueClipboard.isReadClipboardAllowed = async () => {
try {
const clipboardAccessAllowed = await isClipboardAccessAllowed();
if (clipboardAccessAllowed === null) {
await setIsClipboardAccessAllowed(JSON.stringify(true));
return true;
}
return !!JSON.parse(clipboardAccessAllowed);
} catch {
await setIsClipboardAccessAllowed(JSON.stringify(true));
return true;
}
};
BlueClipboard.setReadClipboardAllowed = value => {
setIsClipboardAccessAllowed(JSON.stringify(!!value));
};
BlueClipboard.getClipboardContent = async () => {
const isAllowed = await BlueClipboard.isReadClipboardAllowed();
if (isAllowed) {
return Clipboard.getString();
} else {
return '';
}
};
}
BlueClipboard.default = new BlueClipboard();
export default BlueClipboard;

View file

@ -1,11 +1,38 @@
import QuickActions from 'react-native-quick-actions';
import { Platform } from 'react-native';
import { formatBalance } from '../loc';
import AsyncStorage from '@react-native-community/async-storage';
export default class DeviceQuickActions {
static shared = new DeviceQuickActions();
static STORAGE_KEY = 'DeviceQuickActionsEnabled';
wallets;
static setEnabled(enabled = true) {
return AsyncStorage.setItem(DeviceQuickActions.STORAGE_KEY, JSON.stringify(enabled)).then(() => {
if (!enabled) {
DeviceQuickActions.clearShortcutItems();
} else {
const BlueApp = require('../BlueApp');
DeviceQuickActions.shared.wallets = BlueApp.getWallets();
DeviceQuickActions.setQuickActions();
}
});
}
static async getEnabled() {
try {
const isEnabled = await AsyncStorage.getItem(DeviceQuickActions.STORAGE_KEY);
if (isEnabled === null) {
await DeviceQuickActions.setEnabled(JSON.stringify(true));
return true;
}
return !!JSON.parse(isEnabled);
} catch {
return true;
}
}
static setWallets(wallets) {
DeviceQuickActions.shared.wallets = wallets.slice(0, 4);
}
@ -14,30 +41,35 @@ export default class DeviceQuickActions {
DeviceQuickActions.shared.wallets = undefined;
}
static setQuickActions() {
static async setQuickActions() {
if (DeviceQuickActions.shared.wallets === undefined) {
return;
}
QuickActions.isSupported((error, _supported) => {
if (error === null) {
const shortcutItems = [];
for (const wallet of DeviceQuickActions.shared.wallets) {
shortcutItems.push({
type: 'Wallets', // Required
title: wallet.getLabel(), // Optional, if empty, `type` will be used instead
subtitle:
wallet.hideBalance || wallet.getBalance() <= 0
? ''
: formatBalance(Number(wallet.getBalance()), wallet.getPreferredBalanceUnit(), true),
userInfo: {
url: `bluewallet://wallet/${wallet.getID()}`, // Provide any custom data like deep linking URL
},
icon: Platform.select({ android: 'quickactions', ios: 'bookmark' }),
});
if (await DeviceQuickActions.getEnabled()) {
QuickActions.isSupported((error, _supported) => {
if (error === null) {
const shortcutItems = [];
for (const wallet of DeviceQuickActions.shared.wallets) {
shortcutItems.push({
type: 'Wallets', // Required
title: wallet.getLabel(), // Optional, if empty, `type` will be used instead
subtitle:
wallet.hideBalance || wallet.getBalance() <= 0
? ''
: formatBalance(Number(wallet.getBalance()), wallet.getPreferredBalanceUnit(), true),
userInfo: {
url: `bluewallet://wallet/${wallet.getID()}`, // Provide any custom data like deep linking URL
},
icon: Platform.select({ android: 'quickactions', ios: 'bookmark' }),
});
}
QuickActions.setShortcutItems(shortcutItems);
}
QuickActions.setShortcutItems(shortcutItems);
}
});
});
} else {
DeviceQuickActions.clearShortcutItems();
}
}
static clearShortcutItems() {

View file

@ -8,6 +8,8 @@
"of": "{number} of {total}",
"ok": "OK",
"storage_is_encrypted": "Your storage is encrypted. Password is required to decrypt it",
"allow": "Allow",
"dont_allow": "Don't Allow",
"yes": "Yes",
"no": "No",
"invalid_animated_qr_code_fragment" : "Invalid animated QRCode fragment, please try again",
@ -273,6 +275,13 @@
"password_explain": "Create the password you will use to decrypt the storage",
"passwords_do_not_match": "Passwords do not match",
"plausible_deniability": "Plausible deniability",
"privacy": "Privacy",
"privacy_read_clipboard": "Read Clipboard",
"privacy_read_clipboard_alert": "BlueWallet will display shortcuts for handling an invoice or address found in your clipboard.",
"privacy_system_settings": "System Settings",
"privacy_quickactions": "Wallet Shortcuts",
"privacy_quickactions_explanation": "Touch and hold the BlueWallet app icon on your Home Screen to quickly view your wallet's balance.",
"privacy_clipboard_explanation": "Provide shortcuts if an address, or invoice, is found in your clipboard.",
"push_notifications": "Push notifications",
"retype_password": "Re-type password",
"save": "Save",

View file

@ -0,0 +1,106 @@
import React, { useEffect, useState } from 'react';
import { ScrollView, TouchableWithoutFeedback, StyleSheet, Linking } from 'react-native';
import { BlueTextHooks, BlueSpacing20, BlueListItem, BlueNavigationStyle, BlueCard } from '../../BlueComponents';
import { useTheme } from '@react-navigation/native';
import loc from '../../loc';
import BlueClipboard from '../../blue_modules/clipboard';
import DeviceQuickActions from '../../class/quick-actions';
const BlueApp = require('../../BlueApp');
const SettingsPrivacy = () => {
const { colors } = useTheme();
const sections = Object.freeze({ ALL: 0, CLIPBOARDREAD: 1, QUICKACTION: 2 });
const [isLoading, setIsLoading] = useState(sections.ALL);
const [isReadClipboardAllowed, setIsReadClipboardAllowed] = useState(false);
const [isQuickActionsEnabled, setIsQuickActionsEnabled] = useState(false);
const [storageIsEncrypted, setStorageIsEncrypted] = useState(true);
useEffect(() => {
(async () => {
try {
setIsReadClipboardAllowed(await BlueClipboard.isReadClipboardAllowed());
setStorageIsEncrypted(await BlueApp.storageIsEncrypted());
setIsQuickActionsEnabled(await DeviceQuickActions.getEnabled());
} catch (e) {
console.log(e);
}
setIsLoading(false);
})();
}, []);
const onValueChange = async value => {
setIsLoading(sections.CLIPBOARDREAD);
try {
await BlueClipboard.setReadClipboardAllowed(value);
setIsReadClipboardAllowed(value);
} catch (e) {
console.log(e);
}
setIsLoading(false);
};
const onQuickActionsValueChange = async value => {
setIsLoading(sections.QUICKACTION);
try {
await DeviceQuickActions.setEnabled(value);
setIsQuickActionsEnabled(value);
} catch (e) {
console.log(e);
}
setIsLoading(false);
};
const stylesWithThemeHook = StyleSheet.create({
root: {
backgroundColor: colors.background,
},
});
const openApplicationSettings = () => {
Linking.openSettings();
};
return (
<ScrollView style={[styles.root, stylesWithThemeHook.root]}>
<BlueListItem
hideChevron
title={loc.settings.privacy_read_clipboard}
Component={TouchableWithoutFeedback}
switch={{ onValueChange, value: isReadClipboardAllowed, disabled: isLoading === sections.ALL }}
/>
<BlueCard>
<BlueTextHooks>{loc.settings.privacy_clipboard_explanation}</BlueTextHooks>
</BlueCard>
<BlueSpacing20 />
{!storageIsEncrypted && (
<>
<BlueListItem
hideChevron
title={loc.settings.privacy_quickactions}
Component={TouchableWithoutFeedback}
switch={{ onValueChange: onQuickActionsValueChange, value: isQuickActionsEnabled, disabled: isLoading === sections.ALL }}
/>
<BlueCard>
<BlueTextHooks>{loc.settings.privacy_quickactions_explanation}</BlueTextHooks>
</BlueCard>
</>
)}
<BlueSpacing20 />
<BlueListItem title={loc.settings.privacy_system_settings} chevron onPress={openApplicationSettings} />
<BlueSpacing20 />
</ScrollView>
);
};
const styles = StyleSheet.create({
root: {
flex: 1,
},
});
SettingsPrivacy.navigationOptions = () => ({
...BlueNavigationStyle(),
title: loc.settings.privacy,
});
export default SettingsPrivacy;

View file

@ -34,6 +34,7 @@ const Settings = () => {
onPress={() => navigate('NotificationSettings')}
chevron
/>
<BlueListItem title={loc.settings.privacy} component={TouchableOpacity} onPress={() => navigate('SettingsPrivacy')} chevron />
<BlueListItem
title={loc.settings.about}
component={TouchableOpacity}