REF: Add Wallet advanced options (#7023)

This commit is contained in:
Marcos Rodriguez Vélez 2024-09-10 17:19:31 -04:00 committed by GitHub
parent 4bc7e53a6e
commit fdfb6d11cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 586 additions and 454 deletions

View file

@ -134,11 +134,11 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa name: BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa
path: ./ios/build/BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa path: ./build/BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa
testflight-upload: testflight-upload:
needs: build needs: build
runs-on: macos-14 runs-on: macos-latest
if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'testflight') if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'testflight')
env: env:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
@ -168,12 +168,12 @@ jobs:
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: BlueWallet.${{ needs.build.outputs.project_version }}(${{ needs.build.outputs.new_build_number }}).ipa name: BlueWallet.${{ needs.build.outputs.project_version }}(${{ needs.build.outputs.new_build_number }}).ipa
path: ./ios/build path: ./
- name: Create App Store Connect API Key JSON - name: Create App Store Connect API Key JSON
run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' > ./ios/appstore_api_key.json run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' > ./appstore_api_key.json
- name: Upload to TestFlight - name: Upload to TestFlight
env: env:
APP_STORE_CONNECT_API_KEY_PATH: $(pwd)/ios/appstore_api_key.p8 APP_STORE_CONNECT_API_KEY_PATH: $(pwd)/appstore_api_key.p8
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
GIT_ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }} GIT_ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }}
GIT_URL: ${{ secrets.GIT_URL }} GIT_URL: ${{ secrets.GIT_URL }}

View file

@ -67,13 +67,12 @@ const isReactNative = typeof navigator !== 'undefined' && navigator?.product ===
export class BlueApp { export class BlueApp {
static FLAG_ENCRYPTED = 'data_encrypted'; static FLAG_ENCRYPTED = 'data_encrypted';
static LNDHUB = 'lndhub'; static LNDHUB = 'lndhub';
static ADVANCED_MODE_ENABLED = 'advancedmodeenabled';
static DO_NOT_TRACK = 'donottrack'; static DO_NOT_TRACK = 'donottrack';
static HANDOFF_STORAGE_KEY = 'HandOff'; static HANDOFF_STORAGE_KEY = 'HandOff';
private static _instance: BlueApp | null = null; private static _instance: BlueApp | null = null;
static keys2migrate = [BlueApp.HANDOFF_STORAGE_KEY, BlueApp.DO_NOT_TRACK, BlueApp.ADVANCED_MODE_ENABLED]; static keys2migrate = [BlueApp.HANDOFF_STORAGE_KEY, BlueApp.DO_NOT_TRACK];
public cachedPassword?: false | string; public cachedPassword?: false | string;
public tx_metadata: TTXMetadata; public tx_metadata: TTXMetadata;
@ -882,17 +881,6 @@ export class BlueApp {
return finalBalance; return finalBalance;
}; };
isAdvancedModeEnabled = async (): Promise<boolean> => {
try {
return !!(await AsyncStorage.getItem(BlueApp.ADVANCED_MODE_ENABLED));
} catch (_) {}
return false;
};
setIsAdvancedModeEnabled = async (value: boolean) => {
await AsyncStorage.setItem(BlueApp.ADVANCED_MODE_ENABLED, value ? '1' : '');
};
isHandoffEnabled = async (): Promise<boolean> => { isHandoffEnabled = async (): Promise<boolean> => {
try { try {
return !!(await AsyncStorage.getItem(BlueApp.HANDOFF_STORAGE_KEY)); return !!(await AsyncStorage.getItem(BlueApp.HANDOFF_STORAGE_KEY));

View file

@ -69,8 +69,6 @@ interface SettingsContextType {
setIsHandOffUseEnabledAsyncStorage: (value: boolean) => Promise<void>; setIsHandOffUseEnabledAsyncStorage: (value: boolean) => Promise<void>;
isPrivacyBlurEnabled: boolean; isPrivacyBlurEnabled: boolean;
setIsPrivacyBlurEnabledState: (value: boolean) => void; setIsPrivacyBlurEnabledState: (value: boolean) => void;
isAdvancedModeEnabled: boolean;
setIsAdvancedModeEnabledStorage: (value: boolean) => Promise<void>;
isDoNotTrackEnabled: boolean; isDoNotTrackEnabled: boolean;
setDoNotTrackStorage: (value: boolean) => Promise<void>; setDoNotTrackStorage: (value: boolean) => Promise<void>;
isWidgetBalanceDisplayAllowed: boolean; isWidgetBalanceDisplayAllowed: boolean;
@ -96,8 +94,6 @@ const defaultSettingsContext: SettingsContextType = {
setIsHandOffUseEnabledAsyncStorage: async () => {}, setIsHandOffUseEnabledAsyncStorage: async () => {},
isPrivacyBlurEnabled: true, isPrivacyBlurEnabled: true,
setIsPrivacyBlurEnabledState: () => {}, setIsPrivacyBlurEnabledState: () => {},
isAdvancedModeEnabled: false,
setIsAdvancedModeEnabledStorage: async () => {},
isDoNotTrackEnabled: false, isDoNotTrackEnabled: false,
setDoNotTrackStorage: async () => {}, setDoNotTrackStorage: async () => {},
isWidgetBalanceDisplayAllowed: true, isWidgetBalanceDisplayAllowed: true,
@ -125,8 +121,6 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
const [isHandOffUseEnabled, setHandOffUseEnabled] = useState<boolean>(false); const [isHandOffUseEnabled, setHandOffUseEnabled] = useState<boolean>(false);
// PrivacyBlur // PrivacyBlur
const [isPrivacyBlurEnabled, setIsPrivacyBlurEnabled] = useState<boolean>(true); const [isPrivacyBlurEnabled, setIsPrivacyBlurEnabled] = useState<boolean>(true);
// AdvancedMode
const [isAdvancedModeEnabled, setIsAdvancedModeEnabled] = useState<boolean>(false);
// DoNotTrack // DoNotTrack
const [isDoNotTrackEnabled, setIsDoNotTrackEnabled] = useState<boolean>(false); const [isDoNotTrackEnabled, setIsDoNotTrackEnabled] = useState<boolean>(false);
// WidgetCommunication // WidgetCommunication
@ -141,19 +135,10 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
const [isTotalBalanceEnabled, setIsTotalBalanceEnabled] = useState<boolean>(true); const [isTotalBalanceEnabled, setIsTotalBalanceEnabled] = useState<boolean>(true);
const [totalBalancePreferredUnit, setTotalBalancePreferredUnitState] = useState<BitcoinUnit>(BitcoinUnit.BTC); const [totalBalancePreferredUnit, setTotalBalancePreferredUnitState] = useState<BitcoinUnit>(BitcoinUnit.BTC);
const advancedModeStorage = useAsyncStorage(BlueApp.ADVANCED_MODE_ENABLED);
const languageStorage = useAsyncStorage(STORAGE_KEY); const languageStorage = useAsyncStorage(STORAGE_KEY);
const { walletsInitialized } = useStorage(); const { walletsInitialized } = useStorage();
useEffect(() => { useEffect(() => {
advancedModeStorage
.getItem()
.then(advMode => {
console.debug('SettingsContext advMode:', advMode);
setIsAdvancedModeEnabled(advMode ? JSON.parse(advMode) : false);
})
.catch(error => console.error('Error fetching advanced mode settings:', error));
getIsHandOffUseEnabled() getIsHandOffUseEnabled()
.then(handOff => { .then(handOff => {
console.debug('SettingsContext handOff:', handOff); console.debug('SettingsContext handOff:', handOff);
@ -243,14 +228,6 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setLanguage(newLanguage); setLanguage(newLanguage);
}, []); }, []);
const setIsAdvancedModeEnabledStorage = useCallback(
async (value: boolean) => {
await advancedModeStorage.setItem(JSON.stringify(value));
setIsAdvancedModeEnabled(value);
},
[advancedModeStorage],
);
const setDoNotTrackStorage = useCallback(async (value: boolean) => { const setDoNotTrackStorage = useCallback(async (value: boolean) => {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET); await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
if (value) { if (value) {
@ -321,8 +298,6 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setIsHandOffUseEnabledAsyncStorage, setIsHandOffUseEnabledAsyncStorage,
isPrivacyBlurEnabled, isPrivacyBlurEnabled,
setIsPrivacyBlurEnabledState, setIsPrivacyBlurEnabledState,
isAdvancedModeEnabled,
setIsAdvancedModeEnabledStorage,
isDoNotTrackEnabled, isDoNotTrackEnabled,
setDoNotTrackStorage, setDoNotTrackStorage,
isWidgetBalanceDisplayAllowed, isWidgetBalanceDisplayAllowed,
@ -347,8 +322,6 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setIsHandOffUseEnabledAsyncStorage, setIsHandOffUseEnabledAsyncStorage,
isPrivacyBlurEnabled, isPrivacyBlurEnabled,
setIsPrivacyBlurEnabledState, setIsPrivacyBlurEnabledState,
isAdvancedModeEnabled,
setIsAdvancedModeEnabledStorage,
isDoNotTrackEnabled, isDoNotTrackEnabled,
setDoNotTrackStorage, setDoNotTrackStorage,
isWidgetBalanceDisplayAllowed, isWidgetBalanceDisplayAllowed,

View file

@ -1,5 +1,6 @@
import React, { Ref, useCallback, useMemo } from 'react'; import React, { Ref, useCallback, useMemo } from 'react';
import { Platform, Pressable, TouchableOpacity } from 'react-native'; import { Platform, Pressable, TouchableOpacity } from 'react-native';
import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu';
import { import {
ContextMenuView, ContextMenuView,
RenderItem, RenderItem,
@ -8,7 +9,6 @@ import {
IconConfig, IconConfig,
MenuElementConfig, MenuElementConfig,
} from 'react-native-ios-context-menu'; } from 'react-native-ios-context-menu';
import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu';
import { ToolTipMenuProps, Action } from './types'; import { ToolTipMenuProps, Action } from './types';
import { useSettings } from '../hooks/context/useSettings'; import { useSettings } from '../hooks/context/useSettings';
@ -30,6 +30,7 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
const { language } = useSettings(); const { language } = useSettings();
// Map Menu Items for iOS Context Menu
const mapMenuItemForContextMenuView = useCallback((action: Action) => { const mapMenuItemForContextMenuView = useCallback((action: Action) => {
if (!action.id) return null; if (!action.id) return null;
return { return {
@ -41,14 +42,30 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
}; };
}, []); }, []);
// Map Menu Items for RN Menu (supports subactions and displayInline)
const mapMenuItemForMenuView = useCallback((action: Action): MenuAction | null => { const mapMenuItemForMenuView = useCallback((action: Action): MenuAction | null => {
if (!action.id) return null; if (!action.id) return null;
// Check for subactions
const subactions =
action.subactions?.map(subaction => ({
id: subaction.id.toString(),
title: subaction.text,
subtitle: subaction.subtitle,
image: subaction.icon?.iconValue ? subaction.icon.iconValue : undefined,
state: subaction.menuState === undefined ? undefined : ((subaction.menuState ? 'on' : 'off') as MenuState),
attributes: { disabled: subaction.disabled, destructive: subaction.destructive, hidden: subaction.hidden },
})) || [];
return { return {
id: action.id.toString(), id: action.id.toString(),
title: action.text, title: action.text,
subtitle: action.subtitle,
image: action.icon?.iconValue ? action.icon.iconValue : undefined, image: action.icon?.iconValue ? action.icon.iconValue : undefined,
state: action.menuState === undefined ? undefined : ((action.menuState ? 'on' : 'off') as MenuState), state: action.menuState === undefined ? undefined : ((action.menuState ? 'on' : 'off') as MenuState),
attributes: { disabled: action.disabled }, attributes: { disabled: action.disabled, destructive: action.destructive, hidden: action.hidden },
subactions: subactions.length > 0 ? subactions : undefined,
displayInline: action.displayInline || false,
}; };
}, []); }, []);
@ -98,7 +115,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
); );
const renderContextMenuView = () => { const renderContextMenuView = () => {
console.debug('ToolTipMenu.tsx rendering: renderContextMenuView');
return ( return (
<ContextMenuView <ContextMenuView
lazyPreview lazyPreview
@ -139,7 +155,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
}; };
const renderMenuView = () => { const renderMenuView = () => {
console.debug('ToolTipMenu.tsx rendering: renderMenuView');
return ( return (
<MenuView <MenuView
title={title} title={title}
@ -147,7 +162,7 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
onPressAction={handlePressMenuItemForMenuView} onPressAction={handlePressMenuItemForMenuView}
actions={Platform.OS === 'ios' ? menuViewItemsIOS : menuViewItemsAndroid} actions={Platform.OS === 'ios' ? menuViewItemsIOS : menuViewItemsAndroid}
shouldOpenOnLongPress={!isMenuPrimaryAction} shouldOpenOnLongPress={!isMenuPrimaryAction}
// @ts-ignore: its not in the types but it works // @ts-ignore: Not exposed in types
accessibilityLabel={props.accessibilityLabel} accessibilityLabel={props.accessibilityLabel}
accessibilityHint={props.accessibilityHint} accessibilityHint={props.accessibilityHint}
accessibilityRole={props.accessibilityRole} accessibilityRole={props.accessibilityRole}

View file

@ -289,7 +289,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
handleOnViewOnBlockExplorer, handleOnViewOnBlockExplorer,
], ],
); );
const toolTipActions = useMemo((): Action[] | Action[][] => { const toolTipActions = useMemo((): Action[] => {
const actions: (Action | Action[])[] = []; const actions: (Action | Action[])[] = [];
if (rowTitle !== loc.lnd.expired) { if (rowTitle !== loc.lnd.expired) {
@ -308,7 +308,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
actions.push([CommonToolTipActions.ExpandNote]); actions.push([CommonToolTipActions.ExpandNote]);
} }
return actions as Action[] | Action[][]; return actions as Action[];
}, [item.hash, subtitle, rowTitle, subtitleNumberOfLines]); }, [item.hash, subtitle, rowTitle, subtitleNumberOfLines]);
const accessibilityState = useMemo(() => { const accessibilityState = useMemo(() => {

View file

@ -209,7 +209,7 @@ const styles = StyleSheet.create({
}, },
}); });
const getAvailableActions = ({ allowSignVerifyMessage }: { allowSignVerifyMessage: boolean }): Action[] | Action[][] => { const getAvailableActions = ({ allowSignVerifyMessage }: { allowSignVerifyMessage: boolean }): Action[] => {
const actions = [ const actions = [
{ {
id: actionKeys.CopyToClipboard, id: actionKeys.CopyToClipboard,

View file

@ -1,4 +1,4 @@
import { AccessibilityRole, ViewStyle } from 'react-native'; import { AccessibilityRole, ViewStyle, ColorValue } from 'react-native';
export interface Action { export interface Action {
id: string | number; id: string | number;
@ -7,13 +7,19 @@ export interface Action {
iconValue: string; iconValue: string;
}; };
menuTitle?: string; menuTitle?: string;
subtitle?: string;
menuState?: 'mixed' | boolean | undefined; menuState?: 'mixed' | boolean | undefined;
displayInline?: boolean; // Indicates if subactions should be displayed inline or nested (iOS only)
image?: string;
imageColor?: ColorValue;
destructive?: boolean;
hidden?: boolean;
disabled?: boolean; disabled?: boolean;
displayInline?: boolean; subactions?: Action[]; // Nested/Inline actions (subactions) within an action
} }
export interface ToolTipMenuProps { export interface ToolTipMenuProps {
actions: Action[] | Action[][]; actions: Action[];
children: React.ReactNode; children: React.ReactNode;
enableAndroidRipple?: boolean; enableAndroidRipple?: boolean;
dismissMenu?: () => void; dismissMenu?: () => void;

View file

@ -167,7 +167,8 @@ platform :ios do
type: "development", type: "development",
app_identifier: app_identifier, app_identifier: app_identifier,
readonly: false, # This will regenerate the provisioning profile if needed readonly: false, # This will regenerate the provisioning profile if needed
force_for_new_devices: true # This forces match to add new devices to the profile force_for_new_devices: true,
clone_branch_directly: true
) )
end end
@ -211,6 +212,7 @@ platform :ios do
git_basic_authorization: ENV["GIT_ACCESS_TOKEN"], git_basic_authorization: ENV["GIT_ACCESS_TOKEN"],
git_url: ENV["GIT_URL"], git_url: ENV["GIT_URL"],
type: "appstore", type: "appstore",
clone_branch_directly: true, # Skip if the branch already exists (Exit 128 error)
platform: platform, platform: platform,
app_identifier: app_identifier, app_identifier: app_identifier,
team_id: ENV["ITC_TEAM_ID"], team_id: ENV["ITC_TEAM_ID"],
@ -228,7 +230,8 @@ platform :ios do
type: "development", type: "development",
platform: "catalyst", platform: "catalyst",
app_identifier: app_identifiers, app_identifier: app_identifiers,
readonly: true readonly: true,
clone_branch_directly: true
) )
end end
@ -238,7 +241,9 @@ platform :ios do
type: "appstore", type: "appstore",
platform: "catalyst", platform: "catalyst",
app_identifier: app_identifiers, app_identifier: app_identifiers,
readonly: true readonly: true,
clone_branch_directly: true
) )
end end
@ -250,14 +255,16 @@ platform :ios do
platform: "catalyst", platform: "catalyst",
app_identifier: app_identifier, app_identifier: app_identifier,
readonly: false, readonly: false,
force_for_new_devices: true force_for_new_devices: true,
clone_branch_directly: true
) )
match( match(
type: "appstore", type: "appstore",
platform: "catalyst", platform: "catalyst",
app_identifier: app_identifier, app_identifier: app_identifier,
readonly: false readonly: false,
clone_branch_directly: true
) )
end end
end end
@ -326,8 +333,8 @@ platform :ios do
changelog = ENV["LATEST_COMMIT_MESSAGE"] changelog = ENV["LATEST_COMMIT_MESSAGE"]
upload_to_testflight( upload_to_testflight(
api_key_path: "appstore_api_key.json", api_key_path: "./appstore_api_key.json",
ipa: "./build/BlueWallet.#{ENV['PROJECT_VERSION']}(#{ENV['NEW_BUILD_NUMBER']}).ipa", ipa: "./BlueWallet.#{ENV['PROJECT_VERSION']}(#{ENV['NEW_BUILD_NUMBER']}).ipa",
skip_waiting_for_build_processing: true, # Do not wait for processing skip_waiting_for_build_processing: true, # Do not wait for processing
changelog: changelog changelog: changelog
) )

View file

@ -219,7 +219,6 @@
"about_sm_twitter": "Follow us on Twitter", "about_sm_twitter": "Follow us on Twitter",
"privacy_temporary_screenshots": "Allow Screenshots", "privacy_temporary_screenshots": "Allow Screenshots",
"privacy_temporary_screenshots_instructions": "Screen capture protection will be turned off for this session, allowing you to take screenshots. Once you close and reopen the app, the protection will be automatically turned back on.", "privacy_temporary_screenshots_instructions": "Screen capture protection will be turned off for this session, allowing you to take screenshots. Once you close and reopen the app, the protection will be automatically turned back on.",
"advanced_options": "Advanced Options",
"biometrics": "Biometrics", "biometrics": "Biometrics",
"biometrics_no_longer_available": "Your device settings have changed and no longer match the selected security settings in the app. Please re-enable biometrics or passcode, then restart the app to apply these changes.", "biometrics_no_longer_available": "Your device settings have changed and no longer match the selected security settings in the app. Please re-enable biometrics or passcode, then restart the app to apply these changes.",
"biom_10times": "You have attempted to enter your password 10 times. Would you like to reset your storage? This will remove all wallets and decrypt your storage.", "biom_10times": "You have attempted to enter your password 10 times. Would you like to reset your storage? This will remove all wallets and decrypt your storage.",
@ -272,8 +271,6 @@
"encrypt_use_expl": "{type} will be used to confirm your identity before making a transaction, unlocking, exporting, or deleting a wallet. {type} will not be used to unlock encrypted storage.", "encrypt_use_expl": "{type} will be used to confirm your identity before making a transaction, unlocking, exporting, or deleting a wallet. {type} will not be used to unlock encrypted storage.",
"biometrics_fail": "If {type} is not enabled, or fails to unlock, you can use your device passcode as an alternative.", "biometrics_fail": "If {type} is not enabled, or fails to unlock, you can use your device passcode as an alternative.",
"general": "General", "general": "General",
"general_adv_mode": "Advanced Mode",
"general_adv_mode_e": "When enabled, you will see advanced options such as different wallet types, the ability to specify the LNDHub instance you wish to connect to, and custom entropy during wallet creation.",
"general_continuity": "Continuity", "general_continuity": "Continuity",
"general_continuity_e": "When enabled, you will be able to view selected wallets, and transactions, using your other Apple iCloud connected devices.", "general_continuity_e": "When enabled, you will be able to view selected wallets, and transactions, using your other Apple iCloud connected devices.",
"groundcontrol_explanation": "GroundControl is a free, open-source push notifications server for Bitcoin wallets. You can install your own GroundControl server and put its URL here to not rely on BlueWallets infrastructure. Leave blank to use GroundControls default server.", "groundcontrol_explanation": "GroundControl is a free, open-source push notifications server for Bitcoin wallets. You can install your own GroundControl server and put its URL here to not rely on BlueWallets infrastructure. Leave blank to use GroundControls default server.",
@ -479,7 +476,8 @@
"add_ln_wallet_first": "You must first add a Lightning wallet.", "add_ln_wallet_first": "You must first add a Lightning wallet.",
"identity_pubkey": "Identity Pubkey", "identity_pubkey": "Identity Pubkey",
"xpub_title": "Wallet XPUB", "xpub_title": "Wallet XPUB",
"manage_wallets_search_placeholder": "Search wallets, memos" "manage_wallets_search_placeholder": "Search wallets, memos",
"more_info": "More Info"
}, },
"total_balance_view": { "total_balance_view": {
"view_in_bitcoin": "View in Bitcoin", "view_in_bitcoin": "View in Bitcoin",
@ -489,7 +487,7 @@
"explanation": "View the total balance of all your wallets in the overview screen." "explanation": "View the total balance of all your wallets in the overview screen."
}, },
"multisig": { "multisig": {
"multisig_vault": "Vault", "multisig_vault": "Multisig Vault",
"default_label": "Multisig Vault", "default_label": "Multisig Vault",
"multisig_vault_explain": "Best security for large amounts", "multisig_vault_explain": "Best security for large amounts",
"provide_signature": "Provide signature", "provide_signature": "Provide signature",

View file

@ -1,7 +1,7 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React from 'react'; import React from 'react';
import navigationStyle from '../components/navigationStyle'; import navigationStyle, { CloseButtonPosition } from '../components/navigationStyle';
import { useTheme } from '../components/themes'; import { useTheme } from '../components/themes';
import loc from '../loc'; import loc from '../loc';
import { import {
@ -49,7 +49,7 @@ const AddWalletStack = () => {
name="AddWallet" name="AddWallet"
component={AddComponent} component={AddComponent}
options={navigationStyle({ options={navigationStyle({
headerBackVisible: false, closeButtonPosition: CloseButtonPosition.Left,
title: loc.wallets.add_title, title: loc.wallets.add_title,
})(theme)} })(theme)}
/> />

View file

@ -73,7 +73,6 @@ const DetailViewStackScreensStack = () => {
const { wallets } = useStorage(); const { wallets } = useStorage();
const { isTotalBalanceEnabled } = useSettings(); const { isTotalBalanceEnabled } = useSettings();
const SaveButton = useMemo(() => <HeaderRightButton testID="SaveButton" disabled={true} title={loc.wallets.details_save} />, []);
const DetailButton = useMemo(() => <HeaderRightButton testID="DetailButton" disabled={true} title={loc.send.create_details} />, []); const DetailButton = useMemo(() => <HeaderRightButton testID="DetailButton" disabled={true} title={loc.send.create_details} />, []);
const navigateToAddWallet = useCallback(() => { const navigateToAddWallet = useCallback(() => {
@ -122,7 +121,6 @@ const DetailViewStackScreensStack = () => {
options={navigationStyle({ options={navigationStyle({
headerTitle: loc.wallets.details_title, headerTitle: loc.wallets.details_title,
statusBarStyle: 'auto', statusBarStyle: 'auto',
headerRight: () => SaveButton,
})(theme)} })(theme)}
/> />
<DetailViewStack.Screen <DetailViewStack.Screen
@ -247,7 +245,11 @@ const DetailViewStackScreensStack = () => {
options={navigationStyle({ title: loc.addresses.addresses_title, statusBarStyle: 'auto' })(theme)} options={navigationStyle({ title: loc.addresses.addresses_title, statusBarStyle: 'auto' })(theme)}
/> />
<DetailViewStack.Screen name="AddWalletRoot" component={AddWalletStack} options={NavigationFormModalOptions} /> <DetailViewStack.Screen
name="AddWalletRoot"
component={AddWalletStack}
options={navigationStyle({ closeButtonPosition: CloseButtonPosition.Left, ...NavigationFormModalOptions })(theme)}
/>
<DetailViewStack.Screen name="SendDetailsRoot" component={SendDetailsStack} options={NavigationDefaultOptions} /> <DetailViewStack.Screen name="SendDetailsRoot" component={SendDetailsStack} options={NavigationDefaultOptions} />
<DetailViewStack.Screen name="LNDCreateInvoiceRoot" component={LNDCreateInvoiceRoot} options={NavigationDefaultOptions} /> <DetailViewStack.Screen name="LNDCreateInvoiceRoot" component={LNDCreateInvoiceRoot} options={NavigationDefaultOptions} />
<DetailViewStack.Screen name="ScanLndInvoiceRoot" component={ScanLndInvoiceRoot} options={NavigationDefaultOptions} /> <DetailViewStack.Screen name="ScanLndInvoiceRoot" component={ScanLndInvoiceRoot} options={NavigationDefaultOptions} />

View file

@ -1,6 +1,17 @@
import { useFocusEffect, useRoute } from '@react-navigation/native'; import { useFocusEffect, useRoute } from '@react-navigation/native';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { BackHandler, InteractionManager, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native'; import {
BackHandler,
Image,
InteractionManager,
LayoutAnimation,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import Share from 'react-native-share'; import Share from 'react-native-share';
import * as BlueElectrum from '../../blue_modules/BlueElectrum'; import * as BlueElectrum from '../../blue_modules/BlueElectrum';
@ -24,6 +35,9 @@ import { SuccessView } from '../send/success';
import { useStorage } from '../../hooks/context/useStorage'; import { useStorage } from '../../hooks/context/useStorage';
import { HandOffActivityType } from '../../components/types'; import { HandOffActivityType } from '../../components/types';
import SegmentedControl from '../../components/SegmentControl'; import SegmentedControl from '../../components/SegmentControl';
import ToolTipMenu from '../../components/TooltipMenu';
import { Icon } from '@rneui/themed';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
const segmentControlValues = [loc.wallets.details_address, loc.bip47.payment_code]; const segmentControlValues = [loc.wallets.details_address, loc.bip47.payment_code];
@ -43,9 +57,9 @@ const ReceiveDetails = () => {
const [showConfirmedBalance, setShowConfirmedBalance] = useState(false); const [showConfirmedBalance, setShowConfirmedBalance] = useState(false);
const [showAddress, setShowAddress] = useState(false); const [showAddress, setShowAddress] = useState(false);
const [currentTab, setCurrentTab] = useState(segmentControlValues[0]); const [currentTab, setCurrentTab] = useState(segmentControlValues[0]);
const { goBack, setParams } = useExtendedNavigation(); const { goBack, setParams, setOptions } = useExtendedNavigation();
const bottomModalRef = useRef(null); const bottomModalRef = useRef(null);
const { colors } = useTheme(); const { colors, closeImage } = useTheme();
const [intervalMs, setIntervalMs] = useState(5000); const [intervalMs, setIntervalMs] = useState(5000);
const [eta, setEta] = useState(''); const [eta, setEta] = useState('');
const [initialConfirmed, setInitialConfirmed] = useState(0); const [initialConfirmed, setInitialConfirmed] = useState(0);
@ -79,15 +93,119 @@ const ReceiveDetails = () => {
}, },
}); });
const setAddressBIP21Encoded = useCallback(
addr => {
const newBip21encoded = DeeplinkSchemaMatch.bip21encode(addr);
setParams({ address: addr });
setBip21encoded(newBip21encoded);
setShowAddress(true);
},
[setParams],
);
const obtainWalletAddress = useCallback(async () => {
console.debug('receive/details - componentDidMount');
let newAddress;
if (address) {
setAddressBIP21Encoded(address);
await Notifications.tryToObtainPermissions(receiveAddressButton);
Notifications.majorTomToGroundControl([address], [], []);
} else {
if (wallet.chain === Chain.ONCHAIN) {
try {
if (!isElectrumDisabled) newAddress = await Promise.race([wallet.getAddressAsync(), sleep(1000)]);
} catch (_) {}
if (newAddress === undefined) {
// either sleep expired or getAddressAsync threw an exception
console.warn('either sleep expired or getAddressAsync threw an exception');
newAddress = wallet._getExternalAddressByIndex(wallet.getNextFreeAddressIndex());
} else {
saveToDisk(); // caching whatever getAddressAsync() generated internally
}
} else if (wallet.chain === Chain.OFFCHAIN) {
try {
await Promise.race([wallet.getAddressAsync(), sleep(1000)]);
newAddress = wallet.getAddress();
} catch (_) {}
if (newAddress === undefined) {
// either sleep expired or getAddressAsync threw an exception
console.warn('either sleep expired or getAddressAsync threw an exception');
newAddress = wallet.getAddress();
} else {
saveToDisk(); // caching whatever getAddressAsync() generated internally
}
}
setAddressBIP21Encoded(newAddress);
await Notifications.tryToObtainPermissions(receiveAddressButton);
Notifications.majorTomToGroundControl([newAddress], [], []);
}
}, [wallet, saveToDisk, address, setAddressBIP21Encoded, isElectrumDisabled, sleep]);
const onEnablePaymentsCodeSwitchValue = useCallback(() => {
if (wallet.allowBIP47()) {
wallet.switchBIP47(!wallet.isBIP47Enabled());
}
saveToDisk();
obtainWalletAddress();
}, [wallet, saveToDisk, obtainWalletAddress]);
useEffect(() => { useEffect(() => {
if (showConfirmedBalance) { if (showConfirmedBalance) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
} }
}, [showConfirmedBalance]); }, [showConfirmedBalance]);
const toolTipActions = useMemo(() => {
const action = CommonToolTipActions.PaymentCode;
action.menuState = wallet.isBIP47Enabled();
return [action];
}, [wallet]);
const onPressMenuItem = useCallback(() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
onEnablePaymentsCodeSwitchValue();
}, [onEnablePaymentsCodeSwitchValue]);
const HeaderRight = useMemo(
() => (
<ToolTipMenu isButton isMenuPrimaryAction onPressMenuItem={onPressMenuItem} actions={[toolTipActions]}>
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} />
</ToolTipMenu>
),
[colors.foregroundColor, onPressMenuItem, toolTipActions],
);
const handleClose = useCallback(() => {
goBack();
}, [goBack]);
const HeaderLeft = useMemo(
() => (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.close}
style={styles.button}
onPress={handleClose}
testID="NavigationCloseButton"
>
<Image source={closeImage} />
</TouchableOpacity>
),
[closeImage, handleClose],
);
useEffect(() => {
wallet.allowBIP47() &&
!wallet.isBIP47Enabled() &&
setOptions({
headerLeft: () => (wallet.isBIP47Enabled() ? null : HeaderLeft),
headerRight: () => (wallet.isBIP47Enabled() ? HeaderLeft : HeaderRight),
});
}, [HeaderLeft, HeaderRight, colors.foregroundColor, setOptions, wallet]);
// re-fetching address balance periodically // re-fetching address balance periodically
useEffect(() => { useEffect(() => {
console.log('receive/details - useEffect'); console.debug('receive/details - useEffect');
const intervalId = setInterval(async () => { const intervalId = setInterval(async () => {
try { try {
@ -95,9 +213,9 @@ const ReceiveDetails = () => {
const addressToUse = address || decoded.address; const addressToUse = address || decoded.address;
if (!addressToUse) return; if (!addressToUse) return;
console.log('checking address', addressToUse, 'for balance...'); console.debug('checking address', addressToUse, 'for balance...');
const balance = await BlueElectrum.getBalanceByAddress(addressToUse); const balance = await BlueElectrum.getBalanceByAddress(addressToUse);
console.log('...got', balance); console.debug('...got', balance);
if (balance.unconfirmed > 0) { if (balance.unconfirmed > 0) {
if (initialConfirmed === 0 && initialUnconfirmed === 0) { if (initialConfirmed === 0 && initialUnconfirmed === 0) {
@ -157,7 +275,7 @@ const ReceiveDetails = () => {
} }
} }
} catch (error) { } catch (error) {
console.log(error); console.debug(error);
} }
}, intervalMs); }, intervalMs);
@ -209,16 +327,6 @@ const ReceiveDetails = () => {
return true; return true;
}; };
const setAddressBIP21Encoded = useCallback(
addr => {
const newBip21encoded = DeeplinkSchemaMatch.bip21encode(addr);
setParams({ address: addr });
setBip21encoded(newBip21encoded);
setShowAddress(true);
},
[setParams],
);
useEffect(() => { useEffect(() => {
BackHandler.addEventListener('hardwareBackPress', handleBackButton); BackHandler.addEventListener('hardwareBackPress', handleBackButton);
@ -256,44 +364,6 @@ const ReceiveDetails = () => {
); );
}; };
const obtainWalletAddress = useCallback(async () => {
console.log('receive/details - componentDidMount');
let newAddress;
if (address) {
setAddressBIP21Encoded(address);
await Notifications.tryToObtainPermissions(receiveAddressButton);
Notifications.majorTomToGroundControl([address], [], []);
} else {
if (wallet.chain === Chain.ONCHAIN) {
try {
if (!isElectrumDisabled) newAddress = await Promise.race([wallet.getAddressAsync(), sleep(1000)]);
} catch (_) {}
if (newAddress === undefined) {
// either sleep expired or getAddressAsync threw an exception
console.warn('either sleep expired or getAddressAsync threw an exception');
newAddress = wallet._getExternalAddressByIndex(wallet.getNextFreeAddressIndex());
} else {
saveToDisk(); // caching whatever getAddressAsync() generated internally
}
} else if (wallet.chain === Chain.OFFCHAIN) {
try {
await Promise.race([wallet.getAddressAsync(), sleep(1000)]);
newAddress = wallet.getAddress();
} catch (_) {}
if (newAddress === undefined) {
// either sleep expired or getAddressAsync threw an exception
console.warn('either sleep expired or getAddressAsync threw an exception');
newAddress = wallet.getAddress();
} else {
saveToDisk(); // caching whatever getAddressAsync() generated internally
}
}
setAddressBIP21Encoded(newAddress);
await Notifications.tryToObtainPermissions(receiveAddressButton);
Notifications.majorTomToGroundControl([newAddress], [], []);
}
}, [wallet, saveToDisk, address, setAddressBIP21Encoded, isElectrumDisabled, sleep]);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
const task = InteractionManager.runAfterInteractions(async () => { const task = InteractionManager.runAfterInteractions(async () => {
@ -357,7 +427,7 @@ const ReceiveDetails = () => {
const handleShareButtonPressed = () => { const handleShareButtonPressed = () => {
Share.open({ message: currentTab === loc.wallets.details_address ? bip21encoded : wallet.getBIP47PaymentCode() }).catch(error => Share.open({ message: currentTab === loc.wallets.details_address ? bip21encoded : wallet.getBIP47PaymentCode() }).catch(error =>
console.log(error), console.debug(error),
); );
}; };

View file

@ -16,14 +16,7 @@ const styles = StyleSheet.create({
const GeneralSettings: React.FC = () => { const GeneralSettings: React.FC = () => {
const { wallets } = useStorage(); const { wallets } = useStorage();
const { const { isHandOffUseEnabled, setIsHandOffUseEnabledAsyncStorage, isLegacyURv1Enabled, setIsLegacyURv1EnabledStorage } = useSettings();
isAdvancedModeEnabled,
setIsAdvancedModeEnabledStorage,
isHandOffUseEnabled,
setIsHandOffUseEnabledAsyncStorage,
isLegacyURv1Enabled,
setIsLegacyURv1EnabledStorage,
} = useSettings();
const { navigate } = useNavigation(); const { navigate } = useNavigation();
const { colors } = useTheme(); const { colors } = useTheme();
@ -64,14 +57,6 @@ const GeneralSettings: React.FC = () => {
<BlueSpacing20 /> <BlueSpacing20 />
</> </>
) : null} ) : null}
<ListItem
Component={PressableWrapper}
title={loc.settings.general_adv_mode}
switch={{ onValueChange: setIsAdvancedModeEnabledStorage, value: isAdvancedModeEnabled, testID: 'AdvancedMode' }}
/>
<BlueCard>
<BlueText>{loc.settings.general_adv_mode_e}</BlueText>
</BlueCard>
<BlueSpacing20 /> <BlueSpacing20 />
<ListItem <ListItem
Component={PressableWrapper} Component={PressableWrapper}

View file

@ -1,6 +1,6 @@
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useReducer } from 'react'; import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
Alert, Alert,
@ -9,7 +9,6 @@ import {
Platform, Platform,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
Text,
TextInput, TextInput,
useColorScheme, useColorScheme,
View, View,
@ -21,13 +20,15 @@ import { BlueButtonLink, BlueFormLabel, BlueSpacing20, BlueSpacing40, BlueText }
import { BlueApp, HDSegwitBech32Wallet, HDSegwitP2SHWallet, LightningCustodianWallet, SegwitP2SHWallet } from '../../class'; import { BlueApp, HDSegwitBech32Wallet, HDSegwitP2SHWallet, LightningCustodianWallet, SegwitP2SHWallet } from '../../class';
import presentAlert from '../../components/Alert'; import presentAlert from '../../components/Alert';
import Button from '../../components/Button'; import Button from '../../components/Button';
import ListItem from '../../components/ListItem';
import { useTheme } from '../../components/themes'; import { useTheme } from '../../components/themes';
import WalletButton from '../../components/WalletButton'; import WalletButton from '../../components/WalletButton';
import loc from '../../loc'; import loc from '../../loc';
import { Chain } from '../../models/bitcoinUnits'; import { Chain } from '../../models/bitcoinUnits';
import { useStorage } from '../../hooks/context/useStorage'; import { useStorage } from '../../hooks/context/useStorage';
import { useSettings } from '../../hooks/context/useSettings'; import ToolTipMenu from '../../components/TooltipMenu';
import { Icon } from '@rneui/themed';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { Action } from '../../components/types';
enum ButtonSelected { enum ButtonSelected {
// @ts-ignore: Return later to update // @ts-ignore: Return later to update
@ -43,7 +44,6 @@ interface State {
selectedIndex: number; selectedIndex: number;
label: string; label: string;
selectedWalletType: ButtonSelected; selectedWalletType: ButtonSelected;
backdoorPressed: number;
entropy: Buffer | undefined; entropy: Buffer | undefined;
entropyButtonText: string; entropyButtonText: string;
} }
@ -54,13 +54,12 @@ const ActionTypes = {
SET_SELECTED_INDEX: 'SET_SELECTED_INDEX', SET_SELECTED_INDEX: 'SET_SELECTED_INDEX',
SET_LABEL: 'SET_LABEL', SET_LABEL: 'SET_LABEL',
SET_SELECTED_WALLET_TYPE: 'SET_SELECTED_WALLET_TYPE', SET_SELECTED_WALLET_TYPE: 'SET_SELECTED_WALLET_TYPE',
INCREMENT_BACKDOOR_PRESSED: 'INCREMENT_BACKDOOR_PRESSED',
SET_ENTROPY: 'SET_ENTROPY', SET_ENTROPY: 'SET_ENTROPY',
SET_ENTROPY_BUTTON_TEXT: 'SET_ENTROPY_BUTTON_TEXT', SET_ENTROPY_BUTTON_TEXT: 'SET_ENTROPY_BUTTON_TEXT',
} as const; } as const;
type ActionTypes = (typeof ActionTypes)[keyof typeof ActionTypes]; type ActionTypes = (typeof ActionTypes)[keyof typeof ActionTypes];
interface Action { interface TAction {
type: ActionTypes; type: ActionTypes;
payload?: any; payload?: any;
} }
@ -71,25 +70,22 @@ const initialState: State = {
selectedIndex: 0, selectedIndex: 0,
label: '', label: '',
selectedWalletType: ButtonSelected.ONCHAIN, selectedWalletType: ButtonSelected.ONCHAIN,
backdoorPressed: 1,
entropy: undefined, entropy: undefined,
entropyButtonText: loc.wallets.add_entropy_provide, entropyButtonText: loc.wallets.add_entropy_provide,
}; };
const walletReducer = (state: State, action: Action): State => { const walletReducer = (state: State, action: TAction): State => {
switch (action.type) { switch (action.type) {
case ActionTypes.SET_LOADING: case ActionTypes.SET_LOADING:
return { ...state, isLoading: action.payload }; return { ...state, isLoading: action.payload };
case ActionTypes.SET_WALLET_BASE_URI: case ActionTypes.SET_WALLET_BASE_URI:
return { ...state, walletBaseURI: action.payload }; return { ...state, walletBaseURI: action.payload };
case ActionTypes.SET_SELECTED_INDEX: case ActionTypes.SET_SELECTED_INDEX:
return { ...state, selectedIndex: action.payload }; return { ...state, selectedIndex: action.payload, selectedWalletType: ButtonSelected.ONCHAIN };
case ActionTypes.SET_LABEL: case ActionTypes.SET_LABEL:
return { ...state, label: action.payload }; return { ...state, label: action.payload };
case ActionTypes.SET_SELECTED_WALLET_TYPE: case ActionTypes.SET_SELECTED_WALLET_TYPE:
return { ...state, selectedWalletType: action.payload }; return { ...state, selectedWalletType: action.payload };
case ActionTypes.INCREMENT_BACKDOOR_PRESSED:
return { ...state, backdoorPressed: state.backdoorPressed + 1 };
case ActionTypes.SET_ENTROPY: case ActionTypes.SET_ENTROPY:
return { ...state, entropy: action.payload }; return { ...state, entropy: action.payload };
case ActionTypes.SET_ENTROPY_BUTTON_TEXT: case ActionTypes.SET_ENTROPY_BUTTON_TEXT:
@ -111,10 +107,9 @@ const WalletsAdd: React.FC = () => {
const selectedWalletType = state.selectedWalletType; const selectedWalletType = state.selectedWalletType;
const entropy = state.entropy; const entropy = state.entropy;
const entropyButtonText = state.entropyButtonText; const entropyButtonText = state.entropyButtonText;
//
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
//
const { addWallet, saveToDisk } = useStorage(); const { addWallet, saveToDisk } = useStorage();
const { isAdvancedModeEnabled } = useSettings();
const { navigate, goBack, setOptions } = useNavigation(); const { navigate, goBack, setOptions } = useNavigation();
const stylesHook = { const stylesHook = {
advancedText: { advancedText: {
@ -138,20 +133,7 @@ const WalletsAdd: React.FC = () => {
}, },
}; };
useEffect(() => { const entropyGenerated = useCallback((newEntropy: Buffer) => {
AsyncStorage.getItem(BlueApp.LNDHUB)
.then(url => (url ? setWalletBaseURI(url) : setWalletBaseURI('')))
.catch(() => setWalletBaseURI(''))
.finally(() => setIsLoading(false));
}, []);
useEffect(() => {
setOptions({
statusBarStyle: Platform.select({ ios: 'light', default: colorScheme === 'dark' ? 'light' : 'dark' }),
});
}, [colorScheme, setOptions]);
const entropyGenerated = (newEntropy: Buffer) => {
let entropyTitle; let entropyTitle;
if (!newEntropy) { if (!newEntropy) {
entropyTitle = loc.wallets.add_entropy_provide; entropyTitle = loc.wallets.add_entropy_provide;
@ -162,7 +144,127 @@ const WalletsAdd: React.FC = () => {
} }
setEntropy(newEntropy); setEntropy(newEntropy);
setEntropyButtonText(entropyTitle); setEntropyButtonText(entropyTitle);
}; }, []);
const navigateToEntropy = useCallback(() => {
Alert.alert(
loc.wallets.add_wallet_seed_length,
loc.wallets.add_wallet_seed_length_message,
[
{
text: loc._.cancel,
onPress: () => {},
style: 'default',
},
{
text: loc.wallets.add_wallet_seed_length_12,
onPress: () => {
// @ts-ignore: Return later to update
navigate('ProvideEntropy', { onGenerated: entropyGenerated, words: 12 });
},
style: 'default',
},
{
text: loc.wallets.add_wallet_seed_length_24,
onPress: () => {
// @ts-ignore: Return later to update
navigate('ProvideEntropy', { onGenerated: entropyGenerated, words: 24 });
},
style: 'default',
},
],
{ cancelable: true },
);
}, [entropyGenerated, navigate]);
const toolTipActions = useMemo(() => {
const walletSubactions: Action[] = [
{
id: HDSegwitBech32Wallet.type,
text: `${loc.multisig.native_segwit_title}`,
subtitle: 'p2wsh/HD',
menuState: selectedIndex === 0 && selectedWalletType === ButtonSelected.ONCHAIN,
},
{
id: SegwitP2SHWallet.type,
text: `${loc.multisig.wrapped_segwit_title}`,
subtitle: 'p2sh-p2wsh/HD',
menuState: selectedIndex === 1 && selectedWalletType === ButtonSelected.ONCHAIN,
},
{
id: HDSegwitP2SHWallet.type,
text: `${loc.multisig.legacy_title}`,
subtitle: 'p2sh/non-HD',
menuState: selectedIndex === 2 && selectedWalletType === ButtonSelected.ONCHAIN,
},
{
id: LightningCustodianWallet.type,
text: LightningCustodianWallet.typeReadable,
menuState: selectedWalletType === ButtonSelected.OFFCHAIN,
},
];
const walletAction: Action = {
id: 'wallets',
text: loc.multisig.wallet_type,
subactions: walletSubactions,
displayInline: true,
};
const entropyAction = {
...CommonToolTipActions.Entropy,
text: entropyButtonText,
menuState: false,
};
return [walletAction, entropyAction];
}, [entropyButtonText, selectedIndex, selectedWalletType]);
const handleOnLightningButtonPressed = useCallback(() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setSelectedWalletType(ButtonSelected.OFFCHAIN);
}, []);
const HeaderRight = useMemo(
() => (
<ToolTipMenu
isButton
isMenuPrimaryAction
onPressMenuItem={(id: string) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
if (id === HDSegwitBech32Wallet.type) {
setSelectedIndex(0);
} else if (id === SegwitP2SHWallet.type) {
setSelectedIndex(1);
} else if (id === HDSegwitP2SHWallet.type) {
setSelectedIndex(2);
} else if (id === LightningCustodianWallet.type) {
handleOnLightningButtonPressed();
} else if (id === CommonToolTipActions.Entropy.id) {
navigateToEntropy();
}
}}
actions={toolTipActions}
>
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} />
</ToolTipMenu>
),
[colors.foregroundColor, handleOnLightningButtonPressed, navigateToEntropy, toolTipActions],
);
useEffect(() => {
setOptions({
headerRight: () => HeaderRight,
statusBarStyle: Platform.select({ ios: 'light', default: colorScheme === 'dark' ? 'light' : 'dark' }),
});
}, [HeaderRight, colorScheme, colors.foregroundColor, navigateToEntropy, setOptions, toolTipActions]);
useEffect(() => {
AsyncStorage.getItem(BlueApp.LNDHUB)
.then(url => (url ? setWalletBaseURI(url) : setWalletBaseURI('')))
.catch(() => setWalletBaseURI(''))
.finally(() => setIsLoading(false));
}, []);
const setIsLoading = (value: boolean) => { const setIsLoading = (value: boolean) => {
dispatch({ type: 'SET_LOADING', payload: value }); dispatch({ type: 'SET_LOADING', payload: value });
@ -184,10 +286,6 @@ const WalletsAdd: React.FC = () => {
dispatch({ type: 'SET_SELECTED_WALLET_TYPE', payload: value }); dispatch({ type: 'SET_SELECTED_WALLET_TYPE', payload: value });
}; };
const setBackdoorPressed = (value: number) => {
dispatch({ type: 'INCREMENT_BACKDOOR_PRESSED', payload: value });
};
const setEntropy = (value: Buffer) => { const setEntropy = (value: Buffer) => {
dispatch({ type: 'SET_ENTROPY', payload: value }); dispatch({ type: 'SET_ENTROPY', payload: value });
}; };
@ -225,7 +323,6 @@ const WalletsAdd: React.FC = () => {
} catch (e: any) { } catch (e: any) {
console.log(e.toString()); console.log(e.toString());
presentAlert({ message: e.toString() }); presentAlert({ message: e.toString() });
goBack();
return; return;
} }
} else { } else {
@ -291,37 +388,6 @@ const WalletsAdd: React.FC = () => {
}); });
}; };
const navigateToEntropy = () => {
Alert.alert(
loc.wallets.add_wallet_seed_length,
loc.wallets.add_wallet_seed_length_message,
[
{
text: loc._.cancel,
onPress: () => {},
style: 'default',
},
{
text: loc.wallets.add_wallet_seed_length_12,
onPress: () => {
// @ts-ignore: Return later to update
navigate('ProvideEntropy', { onGenerated: entropyGenerated, words: 12 });
},
style: 'default',
},
{
text: loc.wallets.add_wallet_seed_length_24,
onPress: () => {
// @ts-ignore: Return later to update
navigate('ProvideEntropy', { onGenerated: entropyGenerated, words: 24 });
},
style: 'default',
},
],
{ cancelable: true },
);
};
const navigateToImportWallet = () => { const navigateToImportWallet = () => {
// @ts-ignore: Return later to update // @ts-ignore: Return later to update
navigate('ImportWallet'); navigate('ImportWallet');
@ -339,16 +405,6 @@ const WalletsAdd: React.FC = () => {
setSelectedWalletType(ButtonSelected.ONCHAIN); setSelectedWalletType(ButtonSelected.ONCHAIN);
}; };
const handleOnLightningButtonPressed = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
// @ts-ignore: Return later to update
setBackdoorPressed((prevState: number) => {
return prevState + 1;
});
Keyboard.dismiss();
setSelectedWalletType(ButtonSelected.OFFCHAIN);
};
return ( return (
<ScrollView style={stylesHook.root} testID="ScrollView" automaticallyAdjustKeyboardInsets> <ScrollView style={stylesHook.root} testID="ScrollView" automaticallyAdjustKeyboardInsets>
<BlueSpacing20 /> <BlueSpacing20 />
@ -374,12 +430,6 @@ const WalletsAdd: React.FC = () => {
onPress={handleOnBitcoinButtonPressed} onPress={handleOnBitcoinButtonPressed}
size={styles.button} size={styles.button}
/> />
<WalletButton
buttonType="Lightning"
active={selectedWalletType === ButtonSelected.OFFCHAIN}
onPress={handleOnLightningButtonPressed}
size={styles.button}
/>
<WalletButton <WalletButton
buttonType="Vault" buttonType="Vault"
testID="ActivateVaultButton" testID="ActivateVaultButton"
@ -390,65 +440,29 @@ const WalletsAdd: React.FC = () => {
</View> </View>
<View style={styles.advanced}> <View style={styles.advanced}>
{(() => { {selectedWalletType === ButtonSelected.OFFCHAIN && (
if (selectedWalletType === ButtonSelected.ONCHAIN && isAdvancedModeEnabled) { <>
return ( <BlueSpacing20 />
<View> <BlueText>{loc.wallets.add_lndhub}</BlueText>
<BlueSpacing20 /> <View style={[styles.lndUri, stylesHook.lndUri]}>
<Text style={[styles.advancedText, stylesHook.advancedText]}>{loc.settings.advanced_options}</Text> <TextInput
<ListItem value={walletBaseURI}
containerStyle={[styles.noPadding, stylesHook.noPadding]} onChangeText={setWalletBaseURI}
bottomDivider={false} onSubmitEditing={Keyboard.dismiss}
onPress={() => setSelectedIndex(0)} placeholder={loc.wallets.add_lndhub_placeholder}
title={HDSegwitBech32Wallet.typeReadable} clearButtonMode="while-editing"
checkmark={selectedIndex === 0} autoCapitalize="none"
/> textContentType="URL"
<ListItem autoCorrect={false}
containerStyle={[styles.noPadding, stylesHook.noPadding]} placeholderTextColor="#81868e"
bottomDivider={false} style={styles.textInputCommon}
onPress={() => setSelectedIndex(1)} editable={!isLoading}
title={SegwitP2SHWallet.typeReadable} underlineColorAndroid="transparent"
checkmark={selectedIndex === 1} />
/> </View>
<ListItem </>
containerStyle={[styles.noPadding, stylesHook.noPadding]}
bottomDivider={false}
onPress={() => setSelectedIndex(2)}
title={HDSegwitP2SHWallet.typeReadable}
checkmark={selectedIndex === 2}
/>
</View>
);
} else if (selectedWalletType === ButtonSelected.OFFCHAIN) {
return (
<>
<BlueSpacing20 />
<Text style={[styles.advancedText, stylesHook.advancedText]}>{loc.settings.advanced_options}</Text>
<BlueSpacing20 />
<BlueText>{loc.wallets.add_lndhub}</BlueText>
<View style={[styles.lndUri, stylesHook.lndUri]}>
<TextInput
value={walletBaseURI}
onChangeText={setWalletBaseURI}
onSubmitEditing={Keyboard.dismiss}
placeholder={loc.wallets.add_lndhub_placeholder}
clearButtonMode="while-editing"
autoCapitalize="none"
textContentType="URL"
autoCorrect={false}
placeholderTextColor="#81868e"
style={styles.textInputCommon}
editable={!isLoading}
underlineColorAndroid="transparent"
/>
</View>
</>
);
}
})()}
{isAdvancedModeEnabled === true && selectedWalletType === ButtonSelected.ONCHAIN && !isLoading && (
<BlueButtonLink style={styles.import} title={entropyButtonText} onPress={navigateToEntropy} />
)} )}
<BlueSpacing20 /> <BlueSpacing20 />
{!isLoading ? ( {!isLoading ? (
<> <>
@ -508,9 +522,6 @@ const styles = StyleSheet.create({
advanced: { advanced: {
marginHorizontal: 20, marginHorizontal: 20,
}, },
advancedText: {
fontWeight: '500',
},
lndUri: { lndUri: {
flexDirection: 'row', flexDirection: 'row',
borderWidth: 1, borderWidth: 1,
@ -524,9 +535,6 @@ const styles = StyleSheet.create({
import: { import: {
marginVertical: 24, marginVertical: 24,
}, },
noPadding: {
paddingHorizontal: 0,
},
}); });
export default WalletsAdd; export default WalletsAdd;

View file

@ -1,4 +1,4 @@
import React, { useCallback, useRef, useState } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useFocusEffect, useRoute } from '@react-navigation/native'; import { useFocusEffect, useRoute } from '@react-navigation/native';
import { import {
ActivityIndicator, ActivityIndicator,
@ -11,7 +11,6 @@ import {
ListRenderItemInfo, ListRenderItemInfo,
Platform, Platform,
StyleSheet, StyleSheet,
Switch,
Text, Text,
View, View,
} from 'react-native'; } from 'react-native';
@ -20,12 +19,11 @@ import { isDesktop } from '../../blue_modules/environment';
import { encodeUR } from '../../blue_modules/ur'; import { encodeUR } from '../../blue_modules/ur';
import { import {
BlueButtonLink, BlueButtonLink,
BlueCard,
BlueFormMultiInput, BlueFormMultiInput,
BlueLoading, BlueLoading,
BlueSpacing10, BlueSpacing10,
BlueSpacing20, BlueSpacing20,
BlueSpacing40,
BlueText,
BlueTextCentered, BlueTextCentered,
} from '../../BlueComponents'; } from '../../BlueComponents';
import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class'; import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class';
@ -49,14 +47,14 @@ import usePrivacy from '../../hooks/usePrivacy';
import loc from '../../loc'; import loc from '../../loc';
import ActionSheet from '../ActionSheet'; import ActionSheet from '../ActionSheet';
import { useStorage } from '../../hooks/context/useStorage'; import { useStorage } from '../../hooks/context/useStorage';
import { useSettings } from '../../hooks/context/useSettings'; import ToolTipMenu from '../../components/TooltipMenu';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
const ViewEditMultisigCosigners: React.FC = () => { const ViewEditMultisigCosigners: React.FC = () => {
const hasLoaded = useRef(false); const hasLoaded = useRef(false);
const { colors } = useTheme(); const { colors } = useTheme();
const { wallets, setWalletsWithNewOrder, isElectrumDisabled } = useStorage(); const { wallets, setWalletsWithNewOrder, isElectrumDisabled } = useStorage();
const { isBiometricUseCapableAndEnabled } = useBiometrics(); const { isBiometricUseCapableAndEnabled } = useBiometrics();
const { isAdvancedModeEnabled } = useSettings();
const { navigate, dispatch, addListener } = useExtendedNavigation(); const { navigate, dispatch, addListener } = useExtendedNavigation();
const openScannerButtonRef = useRef(); const openScannerButtonRef = useRef();
const route = useRoute(); const route = useRoute();
@ -97,6 +95,9 @@ const ViewEditMultisigCosigners: React.FC = () => {
vaultKeyText: { vaultKeyText: {
color: colors.alternativeTextColor, color: colors.alternativeTextColor,
}, },
askPassphrase: {
backgroundColor: colors.lightButton,
},
vaultKeyCircleSuccess: { vaultKeyCircleSuccess: {
backgroundColor: colors.msSuccessBG, backgroundColor: colors.msSuccessBG,
}, },
@ -523,6 +524,12 @@ const ViewEditMultisigCosigners: React.FC = () => {
const hideShareModal = () => {}; const hideShareModal = () => {};
const toolTipActions = useMemo(() => {
const passphrase = CommonToolTipActions.Passphrase;
passphrase.menuState = askPassphrase;
return [passphrase];
}, [askPassphrase]);
const renderProvideMnemonicsModal = () => { const renderProvideMnemonicsModal = () => {
return ( return (
<BottomModal <BottomModal
@ -545,18 +552,24 @@ const ViewEditMultisigCosigners: React.FC = () => {
</> </>
} }
> >
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered> <>
<BlueSpacing20 /> <ToolTipMenu
<BlueFormMultiInput value={importText} onChangeText={setImportText} /> isButton
{isAdvancedModeEnabled && ( isMenuPrimaryAction
<> onPressMenuItem={(id: string) => {
<BlueSpacing10 /> LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
<View style={styles.row}> setAskPassphrase(!askPassphrase);
<BlueText>{loc.wallets.import_passphrase}</BlueText> }}
<Switch testID="AskPassphrase" value={askPassphrase} onValueChange={setAskPassphrase} /> actions={toolTipActions}
</View> style={[styles.askPassprase, stylesHook.askPassphrase]}
</> >
)} <Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} />
</ToolTipMenu>
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered>
<BlueSpacing20 />
<BlueFormMultiInput value={importText} onChangeText={setImportText} />
</>
</BottomModal> </BottomModal>
); );
}; };
@ -639,10 +652,11 @@ const ViewEditMultisigCosigners: React.FC = () => {
contentInsetAdjustmentBehavior="automatic" contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets automaticallyAdjustContentInsets
keyExtractor={(_item, index) => `${index}`} keyExtractor={(_item, index) => `${index}`}
contentContainerStyle={styles.contentContainerStyle}
/> />
<BlueSpacing10 /> <BlueSpacing10 />
{footer} <BlueCard>{footer}</BlueCard>
<BlueSpacing40 /> <BlueSpacing20 />
{renderProvideMnemonicsModal()} {renderProvideMnemonicsModal()}
@ -665,6 +679,7 @@ const styles = StyleSheet.create({
paddingTop: 32, paddingTop: 32,
minHeight: 370, minHeight: 370,
}, },
contentContainerStyle: { padding: 16 },
modalContent: { modalContent: {
padding: 22, padding: 22,
justifyContent: 'center', justifyContent: 'center',
@ -700,12 +715,8 @@ const styles = StyleSheet.create({
tipLabelText: { tipLabelText: {
fontWeight: '500', fontWeight: '500',
}, },
row: {
flexDirection: 'row', askPassprase: { top: 0, left: 0, justifyContent: 'center', width: 33, height: 33, borderRadius: 33 / 2 },
alignItems: 'center',
marginHorizontal: 16,
justifyContent: 'space-between',
},
}); });
export default ViewEditMultisigCosigners; export default ViewEditMultisigCosigners;

View file

@ -11,7 +11,6 @@ import ListItem from '../../components/ListItem';
import SafeArea from '../../components/SafeArea'; import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes'; import { useTheme } from '../../components/themes';
import loc from '../../loc'; import loc from '../../loc';
import { useSettings } from '../../hooks/context/useSettings';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { AddWalletStackParamList } from '../../navigation/AddWalletStack'; import { AddWalletStackParamList } from '../../navigation/AddWalletStack';
@ -27,7 +26,6 @@ const WalletsAddMultisig: React.FC = () => {
const [m, setM] = useState(2); const [m, setM] = useState(2);
const [n, setN] = useState(3); const [n, setN] = useState(3);
const [format, setFormat] = useState(MultisigHDWallet.FORMAT_P2WSH); const [format, setFormat] = useState(MultisigHDWallet.FORMAT_P2WSH);
const { isAdvancedModeEnabled } = useSettings();
const stylesHook = StyleSheet.create({ const stylesHook = StyleSheet.create({
root: { root: {
@ -202,17 +200,16 @@ const WalletsAddMultisig: React.FC = () => {
</Text> </Text>
</Text> </Text>
</View> </View>
{isAdvancedModeEnabled && ( <View>
<View> <ListItem
<ListItem testID="VaultAdvancedCustomize"
testID="VaultAdvancedCustomize" onPress={showAdvancedOptionsModal}
onPress={showAdvancedOptionsModal} title={loc.multisig.vault_advanced_customize}
title={loc.multisig.vault_advanced_customize} subtitle={`${getCurrentlySelectedFormat('format')}, ${getCurrentlySelectedFormat('quorum')}`}
subtitle={`${getCurrentlySelectedFormat('format')}, ${getCurrentlySelectedFormat('quorum')}`} chevron
chevron />
/> </View>
</View>
)}
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<Button <Button
testID="LetsStart" testID="LetsStart"

View file

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useFocusEffect, useRoute } from '@react-navigation/native'; import { useFocusEffect, useRoute } from '@react-navigation/native';
import { import {
ActivityIndicator, ActivityIndicator,
@ -8,7 +8,6 @@ import {
LayoutAnimation, LayoutAnimation,
Platform, Platform,
StyleSheet, StyleSheet,
Switch,
Text, Text,
TouchableOpacity, TouchableOpacity,
View, View,
@ -17,7 +16,7 @@ import { Icon } from '@rneui/themed';
import A from '../../blue_modules/analytics'; import A from '../../blue_modules/analytics';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { encodeUR } from '../../blue_modules/ur'; import { encodeUR } from '../../blue_modules/ur';
import { BlueButtonLink, BlueFormMultiInput, BlueSpacing10, BlueSpacing20, BlueText, BlueTextCentered } from '../../BlueComponents'; import { BlueButtonLink, BlueFormMultiInput, BlueSpacing10, BlueSpacing20, BlueTextCentered } from '../../BlueComponents';
import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class'; import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class';
import presentAlert from '../../components/Alert'; import presentAlert from '../../components/Alert';
import BottomModal from '../../components/BottomModal'; import BottomModal from '../../components/BottomModal';
@ -35,15 +34,15 @@ import prompt from '../../helpers/prompt';
import usePrivacy from '../../hooks/usePrivacy'; import usePrivacy from '../../hooks/usePrivacy';
import loc from '../../loc'; import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage'; import { useStorage } from '../../hooks/context/useStorage';
import { useSettings } from '../../hooks/context/useSettings';
import { scanQrHelper } from '../../helpers/scan-qr'; import { scanQrHelper } from '../../helpers/scan-qr';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import ToolTipMenu from '../../components/TooltipMenu';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
const staticCache = {}; const staticCache = {};
const WalletsAddMultisigStep2 = () => { const WalletsAddMultisigStep2 = () => {
const { addWallet, saveToDisk, isElectrumDisabled, sleep, currentSharedCosigner, setSharedCosigner } = useStorage(); const { addWallet, saveToDisk, isElectrumDisabled, sleep, currentSharedCosigner, setSharedCosigner } = useStorage();
const { isAdvancedModeEnabled } = useSettings();
const { colors } = useTheme(); const { colors } = useTheme();
const { navigate, navigateToWalletsList } = useExtendedNavigation(); const { navigate, navigateToWalletsList } = useExtendedNavigation();
@ -108,6 +107,9 @@ const WalletsAddMultisigStep2 = () => {
root: { root: {
backgroundColor: colors.elevated, backgroundColor: colors.elevated,
}, },
askPassphrase: {
backgroundColor: colors.lightButton,
},
textDestination: { textDestination: {
color: colors.foregroundColor, color: colors.foregroundColor,
}, },
@ -611,6 +613,12 @@ const WalletsAddMultisigStep2 = () => {
); );
}; };
const toolTipActions = useMemo(() => {
const passphrase = CommonToolTipActions.Passphrase;
passphrase.menuState = askPassphrase;
return [passphrase];
}, [askPassphrase]);
const renderProvideMnemonicsModal = () => { const renderProvideMnemonicsModal = () => {
return ( return (
<BottomModal <BottomModal
@ -648,18 +656,24 @@ const WalletsAddMultisigStep2 = () => {
setAskPassphrase(false); setAskPassphrase(false);
}} }}
> >
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered> <>
<BlueSpacing20 /> <ToolTipMenu
<BlueFormMultiInput value={importText} onChangeText={setImportText} /> isButton
{isAdvancedModeEnabled && ( isMenuPrimaryAction
<> onPressMenuItem={_id => {
<BlueSpacing10 /> LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
<View style={styles.row}> setAskPassphrase(!askPassphrase);
<BlueText>{loc.wallets.import_passphrase}</BlueText> }}
<Switch testID="AskPassphrase" value={askPassphrase} onValueChange={setAskPassphrase} /> actions={toolTipActions}
</View> style={[styles.askPassprase, stylesHook.askPassphrase]}
</> >
)} <Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} />
</ToolTipMenu>
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered>
<BlueSpacing20 />
<BlueFormMultiInput value={importText} onChangeText={setImportText} />
</>
</BottomModal> </BottomModal>
); );
}; };
@ -798,6 +812,8 @@ const styles = StyleSheet.create({
paddingRight: 8, paddingRight: 8,
borderRadius: 4, borderRadius: 4,
}, },
askPassprase: { top: 0, left: 0, justifyContent: 'center', width: 33, height: 33, borderRadius: 33 / 2 },
secretContainer: { secretContainer: {
flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row', flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
justifyContent: 'flex-start', justifyContent: 'flex-start',
@ -829,12 +845,6 @@ const styles = StyleSheet.create({
fontWeight: 'bold', fontWeight: 'bold',
marginLeft: 8, marginLeft: 8,
}, },
row: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 16,
justifyContent: 'space-between',
},
}); });
export default WalletsAddMultisigStep2; export default WalletsAddMultisigStep2;

View file

@ -41,10 +41,9 @@ import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import loc, { formatBalanceWithoutSuffix } from '../../loc'; import loc, { formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { useSettings } from '../../hooks/context/useSettings';
import { useStorage } from '../../hooks/context/useStorage'; import { useStorage } from '../../hooks/context/useStorage';
import { popToTop } from '../../NavigationService'; import { popToTop } from '../../NavigationService';
import { useRoute } from '@react-navigation/native'; import { useFocusEffect, useRoute } from '@react-navigation/native';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
scrollViewContent: { scrollViewContent: {
@ -113,7 +112,6 @@ const WalletDetails = () => {
const wallet = useRef(wallets.find(w => w.getID() === walletID)).current; const wallet = useRef(wallets.find(w => w.getID() === walletID)).current;
const [walletName, setWalletName] = useState(wallet.getLabel()); const [walletName, setWalletName] = useState(wallet.getLabel());
const [useWithHardwareWallet, setUseWithHardwareWallet] = useState(wallet.useWithHardwareWalletEnabled()); const [useWithHardwareWallet, setUseWithHardwareWallet] = useState(wallet.useWithHardwareWalletEnabled());
const { isAdvancedModeEnabled } = useSettings();
const [isBIP47Enabled, setIsBIP47Enabled] = useState(wallet.isBIP47Enabled()); const [isBIP47Enabled, setIsBIP47Enabled] = useState(wallet.isBIP47Enabled());
const [isContactsVisible, setIsContactsVisible] = useState(wallet.allowBIP47() && wallet.isBIP47Enabled()); const [isContactsVisible, setIsContactsVisible] = useState(wallet.allowBIP47() && wallet.isBIP47Enabled());
const [hideTransactionsInWalletsList, setHideTransactionsInWalletsList] = useState(!wallet.getHideTransactionsInWalletsList()); const [hideTransactionsInWalletsList, setHideTransactionsInWalletsList] = useState(!wallet.getHideTransactionsInWalletsList());
@ -138,13 +136,17 @@ const WalletDetails = () => {
setIsContactsVisible(isBIP47Enabled); setIsContactsVisible(isBIP47Enabled);
}, [isBIP47Enabled]); }, [isBIP47Enabled]);
useEffect(() => { useFocusEffect(
if (isAdvancedModeEnabled && wallet.allowMasterFingerprint()) { useCallback(() => {
InteractionManager.runAfterInteractions(() => { const task = InteractionManager.runAfterInteractions(() => {
setMasterFingerprint(wallet.getMasterFingerprintHex()); if (wallet.allowMasterFingerprint()) {
setMasterFingerprint(wallet.getMasterFingerprintHex());
}
}); });
}
}, [isAdvancedModeEnabled, wallet]); return () => task.cancel();
}, [wallet]),
);
const stylesHook = StyleSheet.create({ const stylesHook = StyleSheet.create({
textLabel1: { textLabel1: {
color: colors.feeText, color: colors.feeText,
@ -541,25 +543,21 @@ const WalletDetails = () => {
</View> </View>
</> </>
)} )}
{isAdvancedModeEnabled && ( <View style={styles.row}>
<View style={styles.row}> {wallet.allowMasterFingerprint() && (
{wallet.allowMasterFingerprint() && ( <View style={styles.marginRight16}>
<View style={styles.marginRight16}> <Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.wallets.details_master_fingerprint.toLowerCase()}</Text>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}> <BlueText>{masterFingerprint ?? <ActivityIndicator />}</BlueText>
{loc.wallets.details_master_fingerprint.toLowerCase()} </View>
</Text> )}
<BlueText>{masterFingerprint ?? <ActivityIndicator />}</BlueText>
</View>
)}
{derivationPath && ( {derivationPath && (
<View> <View>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.wallets.details_derivation_path}</Text> <Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.wallets.details_derivation_path}</Text>
<BlueText testID="DerivationPath">{derivationPath}</BlueText> <BlueText testID="DerivationPath">{derivationPath}</BlueText>
</View> </View>
)} )}
</View> </View>
)}
</View> </View>
</BlueCard> </BlueCard>
{(wallet instanceof AbstractHDElectrumWallet || (wallet.type === WatchOnlyWallet.type && wallet.isHd())) && ( {(wallet instanceof AbstractHDElectrumWallet || (wallet.type === WatchOnlyWallet.type && wallet.isHd())) && (

View file

@ -1,6 +1,6 @@
import { useNavigation, useRoute } from '@react-navigation/native'; import { useRoute } from '@react-navigation/native';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { Keyboard, Platform, StyleSheet, Switch, TouchableWithoutFeedback, View } from 'react-native'; import { Keyboard, Platform, StyleSheet, TouchableWithoutFeedback, View, ScrollView } from 'react-native';
import { import {
BlueButtonLink, BlueButtonLink,
@ -8,24 +8,25 @@ import {
BlueFormLabel, BlueFormLabel,
BlueFormMultiInput, BlueFormMultiInput,
BlueSpacing20, BlueSpacing20,
BlueText,
} from '../../BlueComponents'; } from '../../BlueComponents';
import Button from '../../components/Button'; import Button from '../../components/Button';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes'; import { useTheme } from '../../components/themes';
import { requestCameraAuthorization } from '../../helpers/scan-qr'; import { requestCameraAuthorization } from '../../helpers/scan-qr';
import usePrivacy from '../../hooks/usePrivacy'; import usePrivacy from '../../hooks/usePrivacy';
import loc from '../../loc'; import loc from '../../loc';
import { useSettings } from '../../hooks/context/useSettings'; import { Icon } from '@rneui/themed';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { useKeyboard } from '../../hooks/useKeyboard';
import ToolTipMenu from '../../components/TooltipMenu';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
const WalletsImport = () => { const WalletsImport = () => {
const navigation = useNavigation(); const navigation = useExtendedNavigation();
const { colors } = useTheme(); const { colors } = useTheme();
const route = useRoute(); const route = useRoute();
const label = route?.params?.label ?? ''; const label = route?.params?.label ?? '';
const triggerImport = route?.params?.triggerImport ?? false; const triggerImport = route?.params?.triggerImport ?? false;
const scannedData = route?.params?.scannedData ?? ''; const scannedData = route?.params?.scannedData ?? '';
const { isAdvancedModeEnabled } = useSettings();
const [importText, setImportText] = useState(label); const [importText, setImportText] = useState(label);
const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState(false); const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState(false);
const [, setSpeedBackdoor] = useState(0); const [, setSpeedBackdoor] = useState(0);
@ -33,23 +34,18 @@ const WalletsImport = () => {
const [askPassphrase, setAskPassphrase] = useState(false); const [askPassphrase, setAskPassphrase] = useState(false);
const { enableBlur, disableBlur } = usePrivacy(); const { enableBlur, disableBlur } = usePrivacy();
// Styles
const styles = StyleSheet.create({ const styles = StyleSheet.create({
root: { root: {
paddingTop: 10, paddingTop: 10,
backgroundColor: colors.elevated, backgroundColor: colors.elevated,
flex: 1,
}, },
center: { center: {
flex: 1, flex: 1,
marginHorizontal: 16, marginHorizontal: 16,
backgroundColor: colors.elevated, backgroundColor: colors.elevated,
}, },
row: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 16,
marginTop: 10,
justifyContent: 'space-between',
},
}); });
const onBlur = () => { const onBlur = () => {
@ -58,18 +54,18 @@ const WalletsImport = () => {
return valueWithSingleWhitespace; return valueWithSingleWhitespace;
}; };
useKeyboard({
onKeyboardDidShow: () => {
setIsToolbarVisibleForAndroid(true);
},
onKeyboardDidHide: () => {
setIsToolbarVisibleForAndroid(false);
},
});
useEffect(() => { useEffect(() => {
enableBlur(); enableBlur();
const showSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', () =>
setIsToolbarVisibleForAndroid(true),
);
const hideSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', () =>
setIsToolbarVisibleForAndroid(false),
);
return () => { return () => {
showSubscription.remove();
hideSubscription.remove();
disableBlur(); disableBlur();
}; };
}, [disableBlur, enableBlur]); }, [disableBlur, enableBlur]);
@ -125,21 +121,51 @@ const WalletsImport = () => {
}); });
}; };
const toolTipOnPressMenuItem = useCallback(
menuItem => {
if (menuItem === CommonToolTipActions.Passphrase.id) {
setAskPassphrase(!askPassphrase);
} else if (menuItem === CommonToolTipActions.SearchAccount.id) {
setSearchAccounts(!searchAccounts);
}
},
[askPassphrase, searchAccounts],
);
// ToolTipMenu actions for advanced options
const toolTipActions = useMemo(() => {
const askPassphraseAction = CommonToolTipActions.Passphrase;
askPassphraseAction.menuState = askPassphrase;
const searchAccountsAction = CommonToolTipActions.SearchAccount;
searchAccountsAction.menuState = searchAccounts;
return [askPassphraseAction, searchAccountsAction];
}, [askPassphrase, searchAccounts]);
const HeaderRight = useMemo(
() => (
<ToolTipMenu
isButton
testID="HeaderRightButton"
isMenuPrimaryAction
onPressMenuItem={toolTipOnPressMenuItem}
actions={toolTipActions}
>
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} />
</ToolTipMenu>
),
[toolTipOnPressMenuItem, toolTipActions, colors.foregroundColor],
);
// Adding the ToolTipMenu to the header
useEffect(() => {
navigation.setOptions({
headerRight: () => HeaderRight,
});
}, [askPassphrase, searchAccounts, colors.foregroundColor, navigation, toolTipActions, HeaderRight]);
const renderOptionsAndImportButton = ( const renderOptionsAndImportButton = (
<> <>
{isAdvancedModeEnabled && (
<>
<View style={styles.row}>
<BlueText>{loc.wallets.import_passphrase}</BlueText>
<Switch testID="AskPassphrase" value={askPassphrase} onValueChange={setAskPassphrase} />
</View>
<View style={styles.row}>
<BlueText>{loc.wallets.import_search_accounts}</BlueText>
<Switch testID="SearchAccounts" value={searchAccounts} onValueChange={setSearchAccounts} />
</View>
</>
)}
<BlueSpacing20 /> <BlueSpacing20 />
<View style={styles.center}> <View style={styles.center}>
<> <>
@ -157,7 +183,14 @@ const WalletsImport = () => {
); );
return ( return (
<SafeArea style={styles.root}> <ScrollView
contentContainerStyle={styles.root}
automaticallyAdjustContentInsets
automaticallyAdjustsScrollIndicatorInsets
keyboardShouldPersistTaps
automaticallyAdjustKeyboardInsets
contentInsetAdjustmentBehavior="automatic"
>
<BlueSpacing20 /> <BlueSpacing20 />
<TouchableWithoutFeedback accessibilityRole="button" onPress={speedBackdoorTap} testID="SpeedBackdoor"> <TouchableWithoutFeedback accessibilityRole="button" onPress={speedBackdoorTap} testID="SpeedBackdoor">
<BlueFormLabel>{loc.wallets.import_explanation}</BlueFormLabel> <BlueFormLabel>{loc.wallets.import_explanation}</BlueFormLabel>
@ -197,7 +230,7 @@ const WalletsImport = () => {
/> />
), ),
})} })}
</SafeArea> </ScrollView>
); );
}; };

View file

@ -1,17 +1,7 @@
import assert from 'assert'; import assert from 'assert';
import * as bitcoin from 'bitcoinjs-lib'; import * as bitcoin from 'bitcoinjs-lib';
import { import { expectToBeVisible, extractTextFromElementById, hashIt, helperCreateWallet, helperDeleteWallet, sleep, sup, yo } from './helperz';
expectToBeVisible,
extractTextFromElementById,
hashIt,
helperCreateWallet,
helperDeleteWallet,
helperSwitchAdvancedMode,
sleep,
sup,
yo,
} from './helperz';
import { element } from 'detox'; import { element } from 'detox';
/** /**
@ -64,14 +54,8 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('QuickActionsSwitch')).tap(); await element(by.id('QuickActionsSwitch')).tap();
await element(by.id('QuickActionsSwitch')).tap(); await element(by.id('QuickActionsSwitch')).tap();
await device.pressBack(); await device.pressBack();
await device.pressBack();
// enable AdvancedMode
await element(by.id('AdvancedMode')).tap();
await device.pressBack();
// disable it:
await element(by.id('GeneralSettings')).tap();
await element(by.id('AdvancedMode')).tap();
await device.pressBack();
// //
// currency // currency
// change currency to ARS ($) and switch it back to USD ($) // change currency to ARS ($) and switch it back to USD ($)
@ -472,7 +456,6 @@ describe('BlueWallet UI Tests - no wallets', () => {
if (require('fs').existsSync(lockFile)) return console.warn('skipping as it previously passed on Travis'); if (require('fs').existsSync(lockFile)) return console.warn('skipping as it previously passed on Travis');
} }
await device.launchApp({ delete: true }); // reinstalling the app just for any case to clean up app's storage await device.launchApp({ delete: true }); // reinstalling the app just for any case to clean up app's storage
await helperSwitchAdvancedMode();
await yo('WalletsList'); await yo('WalletsList');
await element(by.id('WalletsList')).swipe('left', 'fast', 1); // in case emu screen is small and it doesnt fit await element(by.id('WalletsList')).swipe('left', 'fast', 1); // in case emu screen is small and it doesnt fit
await sleep(200); // Wait until bounce animation finishes. await sleep(200); // Wait until bounce animation finishes.
@ -542,7 +525,6 @@ describe('BlueWallet UI Tests - no wallets', () => {
await device.pressBack(); await device.pressBack();
await helperDeleteWallet('Multisig Vault'); await helperDeleteWallet('Multisig Vault');
await helperSwitchAdvancedMode(); // turn off advanced mode
process.env.TRAVIS && require('fs').writeFileSync(lockFile, '1'); process.env.TRAVIS && require('fs').writeFileSync(lockFile, '1');
}); });
@ -686,9 +668,6 @@ describe('BlueWallet UI Tests - no wallets', () => {
await device.launchApp({ delete: true }); // reinstalling the app just for any case to clean up app's storage await device.launchApp({ delete: true }); // reinstalling the app just for any case to clean up app's storage
await yo('WalletsList'); await yo('WalletsList');
// enable AdvancedMode to see derivation path in wallet details
await helperSwitchAdvancedMode();
await element(by.id('WalletsList')).swipe('left', 'fast', 1); // in case emu screen is small and it doesnt fit await element(by.id('WalletsList')).swipe('left', 'fast', 1); // in case emu screen is small and it doesnt fit
await sleep(200); // Wait until bounce animation finishes. await sleep(200); // Wait until bounce animation finishes.
// going to Import Wallet screen and importing mnemonic // going to Import Wallet screen and importing mnemonic
@ -698,8 +677,10 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('MnemonicInput')).replaceText( await element(by.id('MnemonicInput')).replaceText(
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
); );
await element(by.id('AskPassphrase')).tap(); await element(by.id('HeaderRightButton')).tap();
await element(by.id('SearchAccounts')).tap(); await element(by.text('Passphrase')).tap();
await element(by.id('HeaderRightButton')).tap();
await element(by.text('Search accounts')).tap();
await element(by.id('DoImport')).tap(); await element(by.id('DoImport')).tap();
await sleep(1000); await sleep(1000);
@ -734,7 +715,6 @@ describe('BlueWallet UI Tests - no wallets', () => {
await device.pressBack(); await device.pressBack();
await device.pressBack(); await device.pressBack();
await helperDeleteWallet('Imported HD Legacy (BIP44 P2PKH)'); await helperDeleteWallet('Imported HD Legacy (BIP44 P2PKH)');
await helperSwitchAdvancedMode();
process.env.TRAVIS && require('fs').writeFileSync(lockFile, '1'); process.env.TRAVIS && require('fs').writeFileSync(lockFile, '1');
}); });

View file

@ -382,6 +382,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
if (!(await getSwitchValue('BIP47Switch'))) { if (!(await getSwitchValue('BIP47Switch'))) {
await expect(element(by.text('Contacts'))).not.toBeVisible(); await expect(element(by.text('Contacts'))).not.toBeVisible();
await element(by.id('BIP47Switch')).tap(); await element(by.id('BIP47Switch')).tap();
await element(by.id('WalletDetailsScroll')).swipe('up', 'fast', 1);
await expect(element(by.text('Contacts'))).toBeVisible(); await expect(element(by.text('Contacts'))).toBeVisible();
await element(by.text('Save')).tap(); // automatically goes back 1 screen await element(by.text('Save')).tap(); // automatically goes back 1 screen
await element(by.text('OK')).tap(); await element(by.text('OK')).tap();

View file

@ -142,11 +142,3 @@ export async function helperCreateWallet(walletName) {
await element(by.id('WalletsList')).swipe('right', 'fast', 1); // in case emu screen is small and it doesnt fit await element(by.id('WalletsList')).swipe('right', 'fast', 1); // in case emu screen is small and it doesnt fit
await expect(element(by.id(walletName || 'cr34t3d'))).toBeVisible(); await expect(element(by.id(walletName || 'cr34t3d'))).toBeVisible();
} }
export async function helperSwitchAdvancedMode() {
await element(by.id('SettingsButton')).tap();
await element(by.id('GeneralSettings')).tap();
await element(by.id('AdvancedMode')).tap();
await device.pressBack();
await device.pressBack();
}

View file

@ -11,6 +11,12 @@ const keys = {
ViewInBitcoin: 'viewInBitcoin', ViewInBitcoin: 'viewInBitcoin',
ViewInSats: 'viewInSats', ViewInSats: 'viewInSats',
ViewInFiat: 'viewInFiat', ViewInFiat: 'viewInFiat',
Entropy: 'entropy',
SearchAccount: 'searchAccount',
Passphrase: 'passphrase',
MoreInfo: 'moreInfo',
SaveChanges: 'saveChanges',
PaymentsCode: 'paymentsCode',
}; };
const icons = { const icons = {
@ -35,6 +41,24 @@ const icons = {
ViewInFiat: { ViewInFiat: {
iconValue: 'coloncurrencysign.circle', iconValue: 'coloncurrencysign.circle',
}, },
Entropy: {
iconValue: 'dice',
},
SearchAccount: {
iconValue: 'magnifyingglass',
},
Passphrase: {
iconValue: 'rectangle.and.pencil.and.ellipsis',
},
MoreInfo: {
iconValue: 'info.circle',
},
SaveChanges: {
iconValue: 'checkmark',
},
PaymentsCode: {
iconValue: 'qrcode',
},
}; };
export const CommonToolTipActions = { export const CommonToolTipActions = {
@ -78,7 +102,6 @@ export const CommonToolTipActions = {
text: loc.total_balance_view.view_in_fiat, text: loc.total_balance_view.view_in_fiat,
icon: icons.ViewInFiat, icon: icons.ViewInFiat,
}, },
ViewInSats: { ViewInSats: {
id: keys.ViewInSats, id: keys.ViewInSats,
text: loc.total_balance_view.view_in_sats, text: loc.total_balance_view.view_in_sats,
@ -89,4 +112,39 @@ export const CommonToolTipActions = {
text: loc.total_balance_view.view_in_bitcoin, text: loc.total_balance_view.view_in_bitcoin,
icon: icons.ViewInBitcoin, icon: icons.ViewInBitcoin,
}, },
Entropy: {
id: keys.Entropy,
text: loc.wallets.add_entropy_provide,
icon: icons.Entropy,
menuState: false,
},
SearchAccount: {
id: keys.SearchAccount,
text: loc.wallets.import_search_accounts,
icon: icons.SearchAccount,
menuState: false,
},
Passphrase: {
id: keys.Passphrase,
text: loc.wallets.import_passphrase,
icon: icons.Passphrase,
menuState: false,
},
MoreInfo: {
id: keys.MoreInfo,
text: loc.wallets.more_info,
icon: icons.MoreInfo,
hidden: false,
},
SaveChanges: {
id: keys.SaveChanges,
text: loc._.save,
icon: icons.SaveChanges,
},
PaymentCode: {
id: keys.PaymentsCode,
text: loc.bip47.purpose,
icon: icons.PaymentsCode,
menuState: false,
},
}; };