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
with:
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:
needs: build
runs-on: macos-14
runs-on: macos-latest
if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'testflight')
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
@ -168,12 +168,12 @@ jobs:
uses: actions/download-artifact@v4
with:
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
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
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 }}
GIT_ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }}
GIT_URL: ${{ secrets.GIT_URL }}

View file

@ -67,13 +67,12 @@ const isReactNative = typeof navigator !== 'undefined' && navigator?.product ===
export class BlueApp {
static FLAG_ENCRYPTED = 'data_encrypted';
static LNDHUB = 'lndhub';
static ADVANCED_MODE_ENABLED = 'advancedmodeenabled';
static DO_NOT_TRACK = 'donottrack';
static HANDOFF_STORAGE_KEY = 'HandOff';
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 tx_metadata: TTXMetadata;
@ -882,17 +881,6 @@ export class BlueApp {
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> => {
try {
return !!(await AsyncStorage.getItem(BlueApp.HANDOFF_STORAGE_KEY));

View file

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

View file

@ -1,5 +1,6 @@
import React, { Ref, useCallback, useMemo } from 'react';
import { Platform, Pressable, TouchableOpacity } from 'react-native';
import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu';
import {
ContextMenuView,
RenderItem,
@ -8,7 +9,6 @@ import {
IconConfig,
MenuElementConfig,
} from 'react-native-ios-context-menu';
import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu';
import { ToolTipMenuProps, Action } from './types';
import { useSettings } from '../hooks/context/useSettings';
@ -30,6 +30,7 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
const { language } = useSettings();
// Map Menu Items for iOS Context Menu
const mapMenuItemForContextMenuView = useCallback((action: Action) => {
if (!action.id) return null;
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 => {
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 {
id: action.id.toString(),
title: action.text,
subtitle: action.subtitle,
image: action.icon?.iconValue ? action.icon.iconValue : undefined,
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 = () => {
console.debug('ToolTipMenu.tsx rendering: renderContextMenuView');
return (
<ContextMenuView
lazyPreview
@ -139,7 +155,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
};
const renderMenuView = () => {
console.debug('ToolTipMenu.tsx rendering: renderMenuView');
return (
<MenuView
title={title}
@ -147,7 +162,7 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
onPressAction={handlePressMenuItemForMenuView}
actions={Platform.OS === 'ios' ? menuViewItemsIOS : menuViewItemsAndroid}
shouldOpenOnLongPress={!isMenuPrimaryAction}
// @ts-ignore: its not in the types but it works
// @ts-ignore: Not exposed in types
accessibilityLabel={props.accessibilityLabel}
accessibilityHint={props.accessibilityHint}
accessibilityRole={props.accessibilityRole}

View file

@ -289,7 +289,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
handleOnViewOnBlockExplorer,
],
);
const toolTipActions = useMemo((): Action[] | Action[][] => {
const toolTipActions = useMemo((): Action[] => {
const actions: (Action | Action[])[] = [];
if (rowTitle !== loc.lnd.expired) {
@ -308,7 +308,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
actions.push([CommonToolTipActions.ExpandNote]);
}
return actions as Action[] | Action[][];
return actions as Action[];
}, [item.hash, subtitle, rowTitle, subtitleNumberOfLines]);
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 = [
{
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 {
id: string | number;
@ -7,13 +7,19 @@ export interface Action {
iconValue: string;
};
menuTitle?: string;
subtitle?: string;
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;
displayInline?: boolean;
subactions?: Action[]; // Nested/Inline actions (subactions) within an action
}
export interface ToolTipMenuProps {
actions: Action[] | Action[][];
actions: Action[];
children: React.ReactNode;
enableAndroidRipple?: boolean;
dismissMenu?: () => void;

View file

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

View file

@ -219,7 +219,6 @@
"about_sm_twitter": "Follow us on Twitter",
"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.",
"advanced_options": "Advanced Options",
"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.",
"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.",
"biometrics_fail": "If {type} is not enabled, or fails to unlock, you can use your device passcode as an alternative.",
"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_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.",
@ -479,7 +476,8 @@
"add_ln_wallet_first": "You must first add a Lightning wallet.",
"identity_pubkey": "Identity Pubkey",
"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": {
"view_in_bitcoin": "View in Bitcoin",
@ -489,7 +487,7 @@
"explanation": "View the total balance of all your wallets in the overview screen."
},
"multisig": {
"multisig_vault": "Vault",
"multisig_vault": "Multisig Vault",
"default_label": "Multisig Vault",
"multisig_vault_explain": "Best security for large amounts",
"provide_signature": "Provide signature",

View file

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

View file

@ -73,7 +73,6 @@ const DetailViewStackScreensStack = () => {
const { wallets } = useStorage();
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 navigateToAddWallet = useCallback(() => {
@ -122,7 +121,6 @@ const DetailViewStackScreensStack = () => {
options={navigationStyle({
headerTitle: loc.wallets.details_title,
statusBarStyle: 'auto',
headerRight: () => SaveButton,
})(theme)}
/>
<DetailViewStack.Screen
@ -247,7 +245,11 @@ const DetailViewStackScreensStack = () => {
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="LNDCreateInvoiceRoot" component={LNDCreateInvoiceRoot} options={NavigationDefaultOptions} />
<DetailViewStack.Screen name="ScanLndInvoiceRoot" component={ScanLndInvoiceRoot} options={NavigationDefaultOptions} />

View file

@ -1,6 +1,17 @@
import { useFocusEffect, useRoute } from '@react-navigation/native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { BackHandler, InteractionManager, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
BackHandler,
Image,
InteractionManager,
LayoutAnimation,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import Share from 'react-native-share';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
@ -24,6 +35,9 @@ import { SuccessView } from '../send/success';
import { useStorage } from '../../hooks/context/useStorage';
import { HandOffActivityType } from '../../components/types';
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];
@ -43,9 +57,9 @@ const ReceiveDetails = () => {
const [showConfirmedBalance, setShowConfirmedBalance] = useState(false);
const [showAddress, setShowAddress] = useState(false);
const [currentTab, setCurrentTab] = useState(segmentControlValues[0]);
const { goBack, setParams } = useExtendedNavigation();
const { goBack, setParams, setOptions } = useExtendedNavigation();
const bottomModalRef = useRef(null);
const { colors } = useTheme();
const { colors, closeImage } = useTheme();
const [intervalMs, setIntervalMs] = useState(5000);
const [eta, setEta] = useState('');
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(() => {
if (showConfirmedBalance) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
}
}, [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
useEffect(() => {
console.log('receive/details - useEffect');
console.debug('receive/details - useEffect');
const intervalId = setInterval(async () => {
try {
@ -95,9 +213,9 @@ const ReceiveDetails = () => {
const addressToUse = address || decoded.address;
if (!addressToUse) return;
console.log('checking address', addressToUse, 'for balance...');
console.debug('checking address', addressToUse, 'for balance...');
const balance = await BlueElectrum.getBalanceByAddress(addressToUse);
console.log('...got', balance);
console.debug('...got', balance);
if (balance.unconfirmed > 0) {
if (initialConfirmed === 0 && initialUnconfirmed === 0) {
@ -157,7 +275,7 @@ const ReceiveDetails = () => {
}
}
} catch (error) {
console.log(error);
console.debug(error);
}
}, intervalMs);
@ -209,16 +327,6 @@ const ReceiveDetails = () => {
return true;
};
const setAddressBIP21Encoded = useCallback(
addr => {
const newBip21encoded = DeeplinkSchemaMatch.bip21encode(addr);
setParams({ address: addr });
setBip21encoded(newBip21encoded);
setShowAddress(true);
},
[setParams],
);
useEffect(() => {
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(
useCallback(() => {
const task = InteractionManager.runAfterInteractions(async () => {
@ -357,7 +427,7 @@ const ReceiveDetails = () => {
const handleShareButtonPressed = () => {
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 { wallets } = useStorage();
const {
isAdvancedModeEnabled,
setIsAdvancedModeEnabledStorage,
isHandOffUseEnabled,
setIsHandOffUseEnabledAsyncStorage,
isLegacyURv1Enabled,
setIsLegacyURv1EnabledStorage,
} = useSettings();
const { isHandOffUseEnabled, setIsHandOffUseEnabledAsyncStorage, isLegacyURv1Enabled, setIsLegacyURv1EnabledStorage } = useSettings();
const { navigate } = useNavigation();
const { colors } = useTheme();
@ -64,14 +57,6 @@ const GeneralSettings: React.FC = () => {
<BlueSpacing20 />
</>
) : 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 />
<ListItem
Component={PressableWrapper}

View file

@ -1,6 +1,6 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useReducer } from 'react';
import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
import {
ActivityIndicator,
Alert,
@ -9,7 +9,6 @@ import {
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
useColorScheme,
View,
@ -21,13 +20,15 @@ import { BlueButtonLink, BlueFormLabel, BlueSpacing20, BlueSpacing40, BlueText }
import { BlueApp, HDSegwitBech32Wallet, HDSegwitP2SHWallet, LightningCustodianWallet, SegwitP2SHWallet } from '../../class';
import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
import ListItem from '../../components/ListItem';
import { useTheme } from '../../components/themes';
import WalletButton from '../../components/WalletButton';
import loc from '../../loc';
import { Chain } from '../../models/bitcoinUnits';
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 {
// @ts-ignore: Return later to update
@ -43,7 +44,6 @@ interface State {
selectedIndex: number;
label: string;
selectedWalletType: ButtonSelected;
backdoorPressed: number;
entropy: Buffer | undefined;
entropyButtonText: string;
}
@ -54,13 +54,12 @@ const ActionTypes = {
SET_SELECTED_INDEX: 'SET_SELECTED_INDEX',
SET_LABEL: 'SET_LABEL',
SET_SELECTED_WALLET_TYPE: 'SET_SELECTED_WALLET_TYPE',
INCREMENT_BACKDOOR_PRESSED: 'INCREMENT_BACKDOOR_PRESSED',
SET_ENTROPY: 'SET_ENTROPY',
SET_ENTROPY_BUTTON_TEXT: 'SET_ENTROPY_BUTTON_TEXT',
} as const;
type ActionTypes = (typeof ActionTypes)[keyof typeof ActionTypes];
interface Action {
interface TAction {
type: ActionTypes;
payload?: any;
}
@ -71,25 +70,22 @@ const initialState: State = {
selectedIndex: 0,
label: '',
selectedWalletType: ButtonSelected.ONCHAIN,
backdoorPressed: 1,
entropy: undefined,
entropyButtonText: loc.wallets.add_entropy_provide,
};
const walletReducer = (state: State, action: Action): State => {
const walletReducer = (state: State, action: TAction): State => {
switch (action.type) {
case ActionTypes.SET_LOADING:
return { ...state, isLoading: action.payload };
case ActionTypes.SET_WALLET_BASE_URI:
return { ...state, walletBaseURI: action.payload };
case ActionTypes.SET_SELECTED_INDEX:
return { ...state, selectedIndex: action.payload };
return { ...state, selectedIndex: action.payload, selectedWalletType: ButtonSelected.ONCHAIN };
case ActionTypes.SET_LABEL:
return { ...state, label: action.payload };
case ActionTypes.SET_SELECTED_WALLET_TYPE:
return { ...state, selectedWalletType: action.payload };
case ActionTypes.INCREMENT_BACKDOOR_PRESSED:
return { ...state, backdoorPressed: state.backdoorPressed + 1 };
case ActionTypes.SET_ENTROPY:
return { ...state, entropy: action.payload };
case ActionTypes.SET_ENTROPY_BUTTON_TEXT:
@ -111,10 +107,9 @@ const WalletsAdd: React.FC = () => {
const selectedWalletType = state.selectedWalletType;
const entropy = state.entropy;
const entropyButtonText = state.entropyButtonText;
//
const colorScheme = useColorScheme();
//
const { addWallet, saveToDisk } = useStorage();
const { isAdvancedModeEnabled } = useSettings();
const { navigate, goBack, setOptions } = useNavigation();
const stylesHook = {
advancedText: {
@ -138,20 +133,7 @@ const WalletsAdd: React.FC = () => {
},
};
useEffect(() => {
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) => {
const entropyGenerated = useCallback((newEntropy: Buffer) => {
let entropyTitle;
if (!newEntropy) {
entropyTitle = loc.wallets.add_entropy_provide;
@ -162,7 +144,127 @@ const WalletsAdd: React.FC = () => {
}
setEntropy(newEntropy);
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) => {
dispatch({ type: 'SET_LOADING', payload: value });
@ -184,10 +286,6 @@ const WalletsAdd: React.FC = () => {
dispatch({ type: 'SET_SELECTED_WALLET_TYPE', payload: value });
};
const setBackdoorPressed = (value: number) => {
dispatch({ type: 'INCREMENT_BACKDOOR_PRESSED', payload: value });
};
const setEntropy = (value: Buffer) => {
dispatch({ type: 'SET_ENTROPY', payload: value });
};
@ -225,7 +323,6 @@ const WalletsAdd: React.FC = () => {
} catch (e: any) {
console.log(e.toString());
presentAlert({ message: e.toString() });
goBack();
return;
}
} 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 = () => {
// @ts-ignore: Return later to update
navigate('ImportWallet');
@ -339,16 +405,6 @@ const WalletsAdd: React.FC = () => {
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 (
<ScrollView style={stylesHook.root} testID="ScrollView" automaticallyAdjustKeyboardInsets>
<BlueSpacing20 />
@ -374,12 +430,6 @@ const WalletsAdd: React.FC = () => {
onPress={handleOnBitcoinButtonPressed}
size={styles.button}
/>
<WalletButton
buttonType="Lightning"
active={selectedWalletType === ButtonSelected.OFFCHAIN}
onPress={handleOnLightningButtonPressed}
size={styles.button}
/>
<WalletButton
buttonType="Vault"
testID="ActivateVaultButton"
@ -390,65 +440,29 @@ const WalletsAdd: React.FC = () => {
</View>
<View style={styles.advanced}>
{(() => {
if (selectedWalletType === ButtonSelected.ONCHAIN && isAdvancedModeEnabled) {
return (
<View>
<BlueSpacing20 />
<Text style={[styles.advancedText, stylesHook.advancedText]}>{loc.settings.advanced_options}</Text>
<ListItem
containerStyle={[styles.noPadding, stylesHook.noPadding]}
bottomDivider={false}
onPress={() => setSelectedIndex(0)}
title={HDSegwitBech32Wallet.typeReadable}
checkmark={selectedIndex === 0}
/>
<ListItem
containerStyle={[styles.noPadding, stylesHook.noPadding]}
bottomDivider={false}
onPress={() => setSelectedIndex(1)}
title={SegwitP2SHWallet.typeReadable}
checkmark={selectedIndex === 1}
/>
<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} />
{selectedWalletType === ButtonSelected.OFFCHAIN && (
<>
<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>
</>
)}
<BlueSpacing20 />
{!isLoading ? (
<>
@ -508,9 +522,6 @@ const styles = StyleSheet.create({
advanced: {
marginHorizontal: 20,
},
advancedText: {
fontWeight: '500',
},
lndUri: {
flexDirection: 'row',
borderWidth: 1,
@ -524,9 +535,6 @@ const styles = StyleSheet.create({
import: {
marginVertical: 24,
},
noPadding: {
paddingHorizontal: 0,
},
});
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 {
ActivityIndicator,
@ -11,7 +11,6 @@ import {
ListRenderItemInfo,
Platform,
StyleSheet,
Switch,
Text,
View,
} from 'react-native';
@ -20,12 +19,11 @@ import { isDesktop } from '../../blue_modules/environment';
import { encodeUR } from '../../blue_modules/ur';
import {
BlueButtonLink,
BlueCard,
BlueFormMultiInput,
BlueLoading,
BlueSpacing10,
BlueSpacing20,
BlueSpacing40,
BlueText,
BlueTextCentered,
} from '../../BlueComponents';
import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class';
@ -49,14 +47,14 @@ import usePrivacy from '../../hooks/usePrivacy';
import loc from '../../loc';
import ActionSheet from '../ActionSheet';
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 hasLoaded = useRef(false);
const { colors } = useTheme();
const { wallets, setWalletsWithNewOrder, isElectrumDisabled } = useStorage();
const { isBiometricUseCapableAndEnabled } = useBiometrics();
const { isAdvancedModeEnabled } = useSettings();
const { navigate, dispatch, addListener } = useExtendedNavigation();
const openScannerButtonRef = useRef();
const route = useRoute();
@ -97,6 +95,9 @@ const ViewEditMultisigCosigners: React.FC = () => {
vaultKeyText: {
color: colors.alternativeTextColor,
},
askPassphrase: {
backgroundColor: colors.lightButton,
},
vaultKeyCircleSuccess: {
backgroundColor: colors.msSuccessBG,
},
@ -523,6 +524,12 @@ const ViewEditMultisigCosigners: React.FC = () => {
const hideShareModal = () => {};
const toolTipActions = useMemo(() => {
const passphrase = CommonToolTipActions.Passphrase;
passphrase.menuState = askPassphrase;
return [passphrase];
}, [askPassphrase]);
const renderProvideMnemonicsModal = () => {
return (
<BottomModal
@ -545,18 +552,24 @@ const ViewEditMultisigCosigners: React.FC = () => {
</>
}
>
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered>
<BlueSpacing20 />
<BlueFormMultiInput value={importText} onChangeText={setImportText} />
{isAdvancedModeEnabled && (
<>
<BlueSpacing10 />
<View style={styles.row}>
<BlueText>{loc.wallets.import_passphrase}</BlueText>
<Switch testID="AskPassphrase" value={askPassphrase} onValueChange={setAskPassphrase} />
</View>
</>
)}
<>
<ToolTipMenu
isButton
isMenuPrimaryAction
onPressMenuItem={(id: string) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setAskPassphrase(!askPassphrase);
}}
actions={toolTipActions}
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>
);
};
@ -639,10 +652,11 @@ const ViewEditMultisigCosigners: React.FC = () => {
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets
keyExtractor={(_item, index) => `${index}`}
contentContainerStyle={styles.contentContainerStyle}
/>
<BlueSpacing10 />
{footer}
<BlueSpacing40 />
<BlueCard>{footer}</BlueCard>
<BlueSpacing20 />
{renderProvideMnemonicsModal()}
@ -665,6 +679,7 @@ const styles = StyleSheet.create({
paddingTop: 32,
minHeight: 370,
},
contentContainerStyle: { padding: 16 },
modalContent: {
padding: 22,
justifyContent: 'center',
@ -700,12 +715,8 @@ const styles = StyleSheet.create({
tipLabelText: {
fontWeight: '500',
},
row: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 16,
justifyContent: 'space-between',
},
askPassprase: { top: 0, left: 0, justifyContent: 'center', width: 33, height: 33, borderRadius: 33 / 2 },
});
export default ViewEditMultisigCosigners;

View file

@ -11,7 +11,6 @@ import ListItem from '../../components/ListItem';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes';
import loc from '../../loc';
import { useSettings } from '../../hooks/context/useSettings';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { AddWalletStackParamList } from '../../navigation/AddWalletStack';
@ -27,7 +26,6 @@ const WalletsAddMultisig: React.FC = () => {
const [m, setM] = useState(2);
const [n, setN] = useState(3);
const [format, setFormat] = useState(MultisigHDWallet.FORMAT_P2WSH);
const { isAdvancedModeEnabled } = useSettings();
const stylesHook = StyleSheet.create({
root: {
@ -202,17 +200,16 @@ const WalletsAddMultisig: React.FC = () => {
</Text>
</Text>
</View>
{isAdvancedModeEnabled && (
<View>
<ListItem
testID="VaultAdvancedCustomize"
onPress={showAdvancedOptionsModal}
title={loc.multisig.vault_advanced_customize}
subtitle={`${getCurrentlySelectedFormat('format')}, ${getCurrentlySelectedFormat('quorum')}`}
chevron
/>
</View>
)}
<View>
<ListItem
testID="VaultAdvancedCustomize"
onPress={showAdvancedOptionsModal}
title={loc.multisig.vault_advanced_customize}
subtitle={`${getCurrentlySelectedFormat('format')}, ${getCurrentlySelectedFormat('quorum')}`}
chevron
/>
</View>
<View style={styles.buttonContainer}>
<Button
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 {
ActivityIndicator,
@ -8,7 +8,6 @@ import {
LayoutAnimation,
Platform,
StyleSheet,
Switch,
Text,
TouchableOpacity,
View,
@ -17,7 +16,7 @@ import { Icon } from '@rneui/themed';
import A from '../../blue_modules/analytics';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
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 presentAlert from '../../components/Alert';
import BottomModal from '../../components/BottomModal';
@ -35,15 +34,15 @@ import prompt from '../../helpers/prompt';
import usePrivacy from '../../hooks/usePrivacy';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { useSettings } from '../../hooks/context/useSettings';
import { scanQrHelper } from '../../helpers/scan-qr';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import ToolTipMenu from '../../components/TooltipMenu';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
const staticCache = {};
const WalletsAddMultisigStep2 = () => {
const { addWallet, saveToDisk, isElectrumDisabled, sleep, currentSharedCosigner, setSharedCosigner } = useStorage();
const { isAdvancedModeEnabled } = useSettings();
const { colors } = useTheme();
const { navigate, navigateToWalletsList } = useExtendedNavigation();
@ -108,6 +107,9 @@ const WalletsAddMultisigStep2 = () => {
root: {
backgroundColor: colors.elevated,
},
askPassphrase: {
backgroundColor: colors.lightButton,
},
textDestination: {
color: colors.foregroundColor,
},
@ -611,6 +613,12 @@ const WalletsAddMultisigStep2 = () => {
);
};
const toolTipActions = useMemo(() => {
const passphrase = CommonToolTipActions.Passphrase;
passphrase.menuState = askPassphrase;
return [passphrase];
}, [askPassphrase]);
const renderProvideMnemonicsModal = () => {
return (
<BottomModal
@ -648,18 +656,24 @@ const WalletsAddMultisigStep2 = () => {
setAskPassphrase(false);
}}
>
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered>
<BlueSpacing20 />
<BlueFormMultiInput value={importText} onChangeText={setImportText} />
{isAdvancedModeEnabled && (
<>
<BlueSpacing10 />
<View style={styles.row}>
<BlueText>{loc.wallets.import_passphrase}</BlueText>
<Switch testID="AskPassphrase" value={askPassphrase} onValueChange={setAskPassphrase} />
</View>
</>
)}
<>
<ToolTipMenu
isButton
isMenuPrimaryAction
onPressMenuItem={_id => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setAskPassphrase(!askPassphrase);
}}
actions={toolTipActions}
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>
);
};
@ -798,6 +812,8 @@ const styles = StyleSheet.create({
paddingRight: 8,
borderRadius: 4,
},
askPassprase: { top: 0, left: 0, justifyContent: 'center', width: 33, height: 33, borderRadius: 33 / 2 },
secretContainer: {
flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
justifyContent: 'flex-start',
@ -829,12 +845,6 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
marginLeft: 8,
},
row: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 16,
justifyContent: 'space-between',
},
});
export default WalletsAddMultisigStep2;

View file

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

View file

@ -1,6 +1,6 @@
import { useNavigation, useRoute } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import { Keyboard, Platform, StyleSheet, Switch, TouchableWithoutFeedback, View } from 'react-native';
import { useRoute } from '@react-navigation/native';
import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { Keyboard, Platform, StyleSheet, TouchableWithoutFeedback, View, ScrollView } from 'react-native';
import {
BlueButtonLink,
@ -8,24 +8,25 @@ import {
BlueFormLabel,
BlueFormMultiInput,
BlueSpacing20,
BlueText,
} from '../../BlueComponents';
import Button from '../../components/Button';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes';
import { requestCameraAuthorization } from '../../helpers/scan-qr';
import usePrivacy from '../../hooks/usePrivacy';
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 navigation = useNavigation();
const navigation = useExtendedNavigation();
const { colors } = useTheme();
const route = useRoute();
const label = route?.params?.label ?? '';
const triggerImport = route?.params?.triggerImport ?? false;
const scannedData = route?.params?.scannedData ?? '';
const { isAdvancedModeEnabled } = useSettings();
const [importText, setImportText] = useState(label);
const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState(false);
const [, setSpeedBackdoor] = useState(0);
@ -33,23 +34,18 @@ const WalletsImport = () => {
const [askPassphrase, setAskPassphrase] = useState(false);
const { enableBlur, disableBlur } = usePrivacy();
// Styles
const styles = StyleSheet.create({
root: {
paddingTop: 10,
backgroundColor: colors.elevated,
flex: 1,
},
center: {
flex: 1,
marginHorizontal: 16,
backgroundColor: colors.elevated,
},
row: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 16,
marginTop: 10,
justifyContent: 'space-between',
},
});
const onBlur = () => {
@ -58,18 +54,18 @@ const WalletsImport = () => {
return valueWithSingleWhitespace;
};
useKeyboard({
onKeyboardDidShow: () => {
setIsToolbarVisibleForAndroid(true);
},
onKeyboardDidHide: () => {
setIsToolbarVisibleForAndroid(false);
},
});
useEffect(() => {
enableBlur();
const showSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', () =>
setIsToolbarVisibleForAndroid(true),
);
const hideSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', () =>
setIsToolbarVisibleForAndroid(false),
);
return () => {
showSubscription.remove();
hideSubscription.remove();
disableBlur();
};
}, [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 = (
<>
{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 />
<View style={styles.center}>
<>
@ -157,7 +183,14 @@ const WalletsImport = () => {
);
return (
<SafeArea style={styles.root}>
<ScrollView
contentContainerStyle={styles.root}
automaticallyAdjustContentInsets
automaticallyAdjustsScrollIndicatorInsets
keyboardShouldPersistTaps
automaticallyAdjustKeyboardInsets
contentInsetAdjustmentBehavior="automatic"
>
<BlueSpacing20 />
<TouchableWithoutFeedback accessibilityRole="button" onPress={speedBackdoorTap} testID="SpeedBackdoor">
<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 * as bitcoin from 'bitcoinjs-lib';
import {
expectToBeVisible,
extractTextFromElementById,
hashIt,
helperCreateWallet,
helperDeleteWallet,
helperSwitchAdvancedMode,
sleep,
sup,
yo,
} from './helperz';
import { expectToBeVisible, extractTextFromElementById, hashIt, helperCreateWallet, helperDeleteWallet, sleep, sup, yo } from './helperz';
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 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
// 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');
}
await device.launchApp({ delete: true }); // reinstalling the app just for any case to clean up app's storage
await helperSwitchAdvancedMode();
await yo('WalletsList');
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.
@ -542,7 +525,6 @@ describe('BlueWallet UI Tests - no wallets', () => {
await device.pressBack();
await helperDeleteWallet('Multisig Vault');
await helperSwitchAdvancedMode(); // turn off advanced mode
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 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 sleep(200); // Wait until bounce animation finishes.
// going to Import Wallet screen and importing mnemonic
@ -698,8 +677,10 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('MnemonicInput')).replaceText(
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
);
await element(by.id('AskPassphrase')).tap();
await element(by.id('SearchAccounts')).tap();
await element(by.id('HeaderRightButton')).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 sleep(1000);
@ -734,7 +715,6 @@ describe('BlueWallet UI Tests - no wallets', () => {
await device.pressBack();
await device.pressBack();
await helperDeleteWallet('Imported HD Legacy (BIP44 P2PKH)');
await helperSwitchAdvancedMode();
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'))) {
await expect(element(by.text('Contacts'))).not.toBeVisible();
await element(by.id('BIP47Switch')).tap();
await element(by.id('WalletDetailsScroll')).swipe('up', 'fast', 1);
await expect(element(by.text('Contacts'))).toBeVisible();
await element(by.text('Save')).tap(); // automatically goes back 1 screen
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 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',
ViewInSats: 'viewInSats',
ViewInFiat: 'viewInFiat',
Entropy: 'entropy',
SearchAccount: 'searchAccount',
Passphrase: 'passphrase',
MoreInfo: 'moreInfo',
SaveChanges: 'saveChanges',
PaymentsCode: 'paymentsCode',
};
const icons = {
@ -35,6 +41,24 @@ const icons = {
ViewInFiat: {
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 = {
@ -78,7 +102,6 @@ export const CommonToolTipActions = {
text: loc.total_balance_view.view_in_fiat,
icon: icons.ViewInFiat,
},
ViewInSats: {
id: keys.ViewInSats,
text: loc.total_balance_view.view_in_sats,
@ -89,4 +112,39 @@ export const CommonToolTipActions = {
text: loc.total_balance_view.view_in_bitcoin,
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,
},
};