mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 01:40:12 +01:00
Merge branch 'master' into renovate/react-native-keychain-9.x
This commit is contained in:
commit
39f48cd200
@ -76,7 +76,6 @@ jobs:
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.1.6
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install Dependencies with Bundler
|
||||
run: |
|
||||
@ -178,15 +177,6 @@ jobs:
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.1.6
|
||||
bundler-cache: true
|
||||
|
||||
- name: Cache Ruby Gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gems-
|
||||
|
||||
- name: Install Dependencies with Bundler
|
||||
run: |
|
||||
|
@ -12,26 +12,24 @@ const BlueApp = BlueAppClass.getInstance();
|
||||
*/
|
||||
let userHasOptedOut: boolean = false;
|
||||
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
(async () => {
|
||||
const uniqueID = await getUniqueId();
|
||||
const doNotTrack = await BlueApp.isDoNotTrackEnabled();
|
||||
(async () => {
|
||||
const uniqueID = await getUniqueId();
|
||||
const doNotTrack = await BlueApp.isDoNotTrackEnabled();
|
||||
|
||||
if (doNotTrack) {
|
||||
// dont start Bugsnag at all
|
||||
return;
|
||||
}
|
||||
if (doNotTrack) {
|
||||
// dont start Bugsnag at all
|
||||
return;
|
||||
}
|
||||
|
||||
Bugsnag.start({
|
||||
user: {
|
||||
id: uniqueID,
|
||||
},
|
||||
onError: function (event) {
|
||||
return !userHasOptedOut;
|
||||
},
|
||||
});
|
||||
})();
|
||||
}
|
||||
Bugsnag.start({
|
||||
user: {
|
||||
id: uniqueID,
|
||||
},
|
||||
onError: function (event) {
|
||||
return !userHasOptedOut;
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
const A = async (event: string) => {};
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'react-native-gesture-handler'; // should be on top
|
||||
|
||||
import { CommonActions } from '@react-navigation/native';
|
||||
import React, { lazy, Suspense, useCallback, useEffect, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { AppState, AppStateStatus, Linking } from 'react-native';
|
||||
import A from '../blue_modules/analytics';
|
||||
import { getClipboardContent } from '../blue_modules/clipboard';
|
||||
@ -25,12 +25,10 @@ import { useStorage } from '../hooks/context/useStorage';
|
||||
import RNQRGenerator from 'rn-qr-generator';
|
||||
import presentAlert from './Alert';
|
||||
import useMenuElements from '../hooks/useMenuElements';
|
||||
import { useSettings } from '../hooks/context/useSettings';
|
||||
import useWidgetCommunication from '../hooks/useWidgetCommunication';
|
||||
import useWatchConnectivity from '../hooks/useWatchConnectivity';
|
||||
|
||||
const DeviceQuickActions = lazy(() => import('../components/DeviceQuickActions'));
|
||||
const HandOffComponentListener = lazy(() => import('../components/HandOffComponentListener'));
|
||||
import useDeviceQuickActions from '../hooks/useDeviceQuickActions';
|
||||
import useHandoffListener from '../hooks/useHandoffListener';
|
||||
|
||||
const ClipboardContentType = Object.freeze({
|
||||
BITCOIN: 'BITCOIN',
|
||||
@ -40,12 +38,13 @@ const ClipboardContentType = Object.freeze({
|
||||
const CompanionDelegates = () => {
|
||||
const { wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions, setSharedCosigner } = useStorage();
|
||||
const appState = useRef<AppStateStatus>(AppState.currentState);
|
||||
const { isHandOffUseEnabled, isQuickActionsEnabled } = useSettings();
|
||||
const clipboardContent = useRef<undefined | string>();
|
||||
|
||||
useWatchConnectivity();
|
||||
useWidgetCommunication();
|
||||
useMenuElements();
|
||||
useDeviceQuickActions();
|
||||
useHandoffListener();
|
||||
|
||||
const processPushNotifications = useCallback(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
@ -311,12 +310,7 @@ const CompanionDelegates = () => {
|
||||
};
|
||||
}, [addListeners]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
{isQuickActionsEnabled && <DeviceQuickActions />}
|
||||
{isHandOffUseEnabled && <HandOffComponentListener />}
|
||||
</Suspense>
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default CompanionDelegates;
|
||||
|
@ -6,7 +6,10 @@ import { clearUseURv1, isURv1Enabled, setUseURv1 } from '../../blue_modules/ur';
|
||||
import { BlueApp } from '../../class';
|
||||
import { saveLanguage, STORAGE_KEY } from '../../loc';
|
||||
import { FiatUnit, TFiatUnit } from '../../models/fiatUnit';
|
||||
import { getEnabled as getIsDeviceQuickActionsEnabled, setEnabled as setIsDeviceQuickActionsEnabled } from '../DeviceQuickActions';
|
||||
import {
|
||||
getEnabled as getIsDeviceQuickActionsEnabled,
|
||||
setEnabled as setIsDeviceQuickActionsEnabled,
|
||||
} from '../../hooks/useDeviceQuickActions';
|
||||
import { getIsHandOffUseEnabled, setIsHandOffUseEnabled } from '../HandOffComponent';
|
||||
import { useStorage } from '../../hooks/context/useStorage';
|
||||
import { BitcoinUnit } from '../../models/bitcoinUnits';
|
||||
|
@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export const DeviceQuickActionsStorageKey = 'DeviceQuickActionsEnabled';
|
||||
|
||||
interface DeviceQuickActionsFunctions {
|
||||
popInitialAction: () => void;
|
||||
}
|
||||
|
||||
export const setEnabled = (): void => {};
|
||||
|
||||
export const getEnabled = async (): Promise<boolean> => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const DeviceQuickActions: React.FC & DeviceQuickActionsFunctions = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
DeviceQuickActions.popInitialAction = (): void => {};
|
||||
|
||||
export default DeviceQuickActions;
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import DefaultPreference from 'react-native-default-preference';
|
||||
// @ts-ignore: react-native-handoff is not in the type definition
|
||||
// @ts-ignore: Handoff is not typed
|
||||
import Handoff from 'react-native-handoff';
|
||||
import { useSettings } from '../hooks/context/useSettings';
|
||||
import { GROUP_IO_BLUEWALLET } from '../blue_modules/currency';
|
||||
@ -9,32 +9,32 @@ import { HandOffComponentProps } from './types';
|
||||
|
||||
const HandOffComponent: React.FC<HandOffComponentProps> = props => {
|
||||
const { isHandOffUseEnabled } = useSettings();
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.debug('HandOffComponent: render');
|
||||
}
|
||||
if (isHandOffUseEnabled) {
|
||||
return <Handoff {...props} />;
|
||||
}
|
||||
return null;
|
||||
console.debug('HandOffComponent is rendering.');
|
||||
return isHandOffUseEnabled ? <Handoff {...props} /> : null;
|
||||
};
|
||||
|
||||
const MemoizedHandOffComponent = React.memo(HandOffComponent);
|
||||
|
||||
export const setIsHandOffUseEnabled = async (value: boolean) => {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
await DefaultPreference.set(BlueApp.HANDOFF_STORAGE_KEY, value.toString());
|
||||
console.debug('setIsHandOffUseEnabledAsyncStorage', value);
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
await DefaultPreference.set(BlueApp.HANDOFF_STORAGE_KEY, value.toString());
|
||||
console.debug('setIsHandOffUseEnabled', value);
|
||||
} catch (error) {
|
||||
console.error('Error setting handoff enabled status:', error);
|
||||
throw error; // Propagate error to caller
|
||||
}
|
||||
};
|
||||
|
||||
export const getIsHandOffUseEnabled = async (): Promise<boolean> => {
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
const isEnabledValue = (await DefaultPreference.get(BlueApp.HANDOFF_STORAGE_KEY)) ?? false;
|
||||
console.debug('getIsHandOffUseEnabled', isEnabledValue);
|
||||
return isEnabledValue === 'true';
|
||||
} catch (e) {
|
||||
console.debug('getIsHandOffUseEnabled error', e);
|
||||
const isEnabledValue = await DefaultPreference.get(BlueApp.HANDOFF_STORAGE_KEY);
|
||||
const result = isEnabledValue === 'true';
|
||||
console.debug('getIsHandOffUseEnabled', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error getting handoff enabled status:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
@ -1,73 +0,0 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { NativeEventEmitter, NativeModules } from 'react-native';
|
||||
import { useStorage } from '../hooks/context/useStorage';
|
||||
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
|
||||
import { HandOffActivityType } from './types';
|
||||
|
||||
interface UserActivityData {
|
||||
activityType: HandOffActivityType;
|
||||
userInfo: {
|
||||
address?: string;
|
||||
xpub?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const { EventEmitter } = NativeModules;
|
||||
const eventEmitter = new NativeEventEmitter(EventEmitter);
|
||||
|
||||
const HandOffComponentListener: React.FC = React.memo(() => {
|
||||
const { walletsInitialized } = useStorage();
|
||||
const { navigate } = useExtendedNavigation();
|
||||
|
||||
const onUserActivityOpen = useCallback((data: UserActivityData) => {
|
||||
switch (data.activityType) {
|
||||
case HandOffActivityType.ReceiveOnchain:
|
||||
navigate('ReceiveDetailsRoot', {
|
||||
screen: 'ReceiveDetails',
|
||||
params: {
|
||||
address: data.userInfo.address,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case HandOffActivityType.Xpub:
|
||||
navigate('WalletXpubRoot', {
|
||||
screen: 'WalletXpub',
|
||||
params: {
|
||||
xpub: data.userInfo.xpub,
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.log(`Unhandled activity type: ${data.activityType}`);
|
||||
break;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!walletsInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addListeners = () => {
|
||||
const activitySubscription = eventEmitter.addListener('onUserActivityOpen', onUserActivityOpen);
|
||||
|
||||
// Attempt to fetch the most recent user activity
|
||||
EventEmitter.getMostRecentUserActivity?.()
|
||||
.then(onUserActivityOpen)
|
||||
.catch(() => console.log('No userActivity object sent'));
|
||||
|
||||
return { activitySubscription };
|
||||
};
|
||||
|
||||
const subscriptions = addListeners();
|
||||
|
||||
return () => {
|
||||
subscriptions.activitySubscription?.remove();
|
||||
};
|
||||
}, [walletsInitialized, onUserActivityOpen]);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
export default HandOffComponentListener;
|
@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const HandOffComponentListener: React.FC = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export default HandOffComponentListener;
|
@ -9,9 +9,10 @@ interface HeaderMenuButtonProps {
|
||||
onPressMenuItem: (id: string) => void;
|
||||
actions?: Action[] | Action[][];
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const HeaderMenuButton: React.FC<HeaderMenuButtonProps> = ({ onPressMenuItem, actions, disabled }) => {
|
||||
const HeaderMenuButton: React.FC<HeaderMenuButtonProps> = ({ onPressMenuItem, actions, disabled, title }) => {
|
||||
const { colors } = useTheme();
|
||||
const styleProps = Platform.OS === 'android' ? { iconStyle: { transform: [{ rotate: '90deg' }] } } : {};
|
||||
|
||||
@ -38,6 +39,7 @@ const HeaderMenuButton: React.FC<HeaderMenuButtonProps> = ({ onPressMenuItem, ac
|
||||
isMenuPrimaryAction
|
||||
onPressMenuItem={onPressMenuItem}
|
||||
actions={menuActions}
|
||||
title={title}
|
||||
>
|
||||
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} {...styleProps} />
|
||||
</ToolTipMenu>
|
||||
|
@ -90,7 +90,6 @@ platform :android do
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
desc "Upload APK to BrowserStack and post result as PR comment"
|
||||
lane :upload_to_browserstack_and_comment do
|
||||
Dir.chdir(project_root) do
|
||||
@ -120,30 +119,60 @@ platform :android do
|
||||
browserstack_hashed_id = app_url.gsub('bs://', '')
|
||||
pr_number = ENV['GITHUB_PR_NUMBER']
|
||||
|
||||
comment_identifier = '### APK Successfully Uploaded to BrowserStack'
|
||||
|
||||
comment = <<~COMMENT
|
||||
### APK Successfully Uploaded to BrowserStack
|
||||
|
||||
#{comment_identifier}
|
||||
|
||||
You can test it on the following devices:
|
||||
|
||||
- [Google Pixel 5 (Android 12.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=12.0&device=Google+Pixel+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 7 (Android 13.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Google+Pixel+7&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 8 (Android 14.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Google+Pixel+8&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 3a (Android 9.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=9.0&device=Google+Pixel+3a&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
|
||||
|
||||
- [Samsung Galaxy Z Fold 5 (Android 13.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Z+Fold+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Samsung Galaxy Z Fold 6 (Android 14.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Samsung+Galaxy+Z+Fold+6&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Samsung Galaxy Tab S9 (Android 13.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Tab+S9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Samsung Galaxy Note 9 (Android 8.1)](https://app-live.browserstack.com/dashboard#os=android&os_version=8.1&device=Samsung+Galaxy+Note+9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
|
||||
|
||||
**Filename**: [#{apk_filename}](#{apk_download_url})
|
||||
**BrowserStack App URL**: #{app_url}
|
||||
COMMENT
|
||||
|
||||
# Post PR comment if PR number is available
|
||||
# Delete Previous BrowserStack Comments
|
||||
if pr_number
|
||||
begin
|
||||
sh("GH_TOKEN=#{ENV['GH_TOKEN']} gh pr comment #{pr_number} --body '#{comment}'")
|
||||
UI.success("Posted comment to PR ##{pr_number}")
|
||||
repo = ENV['GITHUB_REPOSITORY'] # Format: "owner/repo"
|
||||
repo_owner, repo_name = repo.split('/')
|
||||
|
||||
UI.message("Fetching existing comments for PR ##{pr_number}...")
|
||||
|
||||
comments_json = `gh api -X GET /repos/#{repo_owner}/#{repo_name}/issues/#{pr_number}/comments`
|
||||
comments = JSON.parse(comments_json)
|
||||
|
||||
comments.each do |comment|
|
||||
if comment['body'].start_with?(comment_identifier)
|
||||
comment_id = comment['id']
|
||||
UI.message("Deleting previous comment ID: #{comment_id}...")
|
||||
`gh api -X DELETE /repos/#{repo_owner}/#{repo_name}/issues/comments/#{comment_id}`
|
||||
UI.success("Deleted comment ID: #{comment_id}")
|
||||
end
|
||||
end
|
||||
|
||||
rescue => e
|
||||
UI.error("Failed to delete previous comments: #{e.message}")
|
||||
end
|
||||
else
|
||||
UI.important("No PR number found. Skipping deletion of previous comments.")
|
||||
end
|
||||
|
||||
# Post New Comment to PR
|
||||
if pr_number
|
||||
begin
|
||||
escaped_comment = comment.gsub("'", "'\\''")
|
||||
sh("GH_TOKEN=#{ENV['GH_TOKEN']} gh pr comment #{pr_number} --body '#{escaped_comment}'")
|
||||
UI.success("Posted new comment to PR ##{pr_number}")
|
||||
rescue => e
|
||||
UI.error("Failed to post comment to PR: #{e.message}")
|
||||
end
|
||||
@ -151,10 +180,9 @@ platform :android do
|
||||
UI.important("No PR number found. Skipping PR comment.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
# ===========================
|
||||
# iOS Lanes
|
||||
# ===========================
|
||||
@ -294,7 +322,7 @@ platform :ios do
|
||||
desc "Install CocoaPods dependencies"
|
||||
lane :install_pods do
|
||||
UI.message("Installing CocoaPods dependencies...")
|
||||
cocoapods(podfile: "ios/Podfile")
|
||||
cocoapods(podfile: "ios/Podfile", deployment: true, clean_install: true)
|
||||
end
|
||||
|
||||
|
||||
@ -412,7 +440,7 @@ lane :build_app_lane do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
# ===========================
|
||||
# Global Lanes
|
||||
# ===========================
|
||||
@ -561,5 +589,5 @@ lane :update_release_notes do |options|
|
||||
UI.error("No localization found for locale #{locale}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,8 +1,8 @@
|
||||
import { useEffect } from 'react';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { CommonActions } from '@react-navigation/native';
|
||||
import { useEffect } from 'react';
|
||||
import { DeviceEventEmitter, Linking, Platform } from 'react-native';
|
||||
import QuickActions from 'react-native-quick-actions';
|
||||
import QuickActions, { ShortcutItem } from 'react-native-quick-actions';
|
||||
import DeeplinkSchemaMatch from '../class/deeplink-schema-match';
|
||||
import { TWallet } from '../class/wallets/types';
|
||||
import useOnAppLaunch from '../hooks/useOnAppLaunch';
|
||||
@ -30,16 +30,15 @@ export async function getEnabled(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
function DeviceQuickActions() {
|
||||
const useDeviceQuickActions = () => {
|
||||
const { wallets, walletsInitialized, isStorageEncrypted, addWallet, saveToDisk, setSharedCosigner } = useStorage();
|
||||
const { preferredFiatCurrency, isQuickActionsEnabled } = useSettings();
|
||||
|
||||
const { isViewAllWalletsEnabled, getSelectedDefaultWallet } = useOnAppLaunch();
|
||||
|
||||
useEffect(() => {
|
||||
if (walletsInitialized) {
|
||||
isStorageEncrypted()
|
||||
.then((value: boolean | undefined | null) => {
|
||||
.then(value => {
|
||||
if (value) {
|
||||
removeShortcuts();
|
||||
} else {
|
||||
@ -48,7 +47,7 @@ function DeviceQuickActions() {
|
||||
})
|
||||
.catch(() => removeShortcuts());
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [wallets, walletsInitialized, preferredFiatCurrency, isStorageEncrypted]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -57,7 +56,7 @@ function DeviceQuickActions() {
|
||||
popInitialShortcutAction().then(popInitialAction);
|
||||
return () => DeviceEventEmitter.removeAllListeners('quickActionShortcut');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [walletsInitialized]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -68,7 +67,7 @@ function DeviceQuickActions() {
|
||||
removeShortcuts();
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isQuickActionsEnabled, walletsInitialized]);
|
||||
|
||||
const popInitialShortcutAction = async (): Promise<any> => {
|
||||
@ -78,7 +77,7 @@ function DeviceQuickActions() {
|
||||
|
||||
const popInitialAction = async (data: any): Promise<void> => {
|
||||
if (data) {
|
||||
const wallet = wallets.find((w: { getID: () => any }) => w.getID() === data.userInfo.url.split('wallet/')[1]);
|
||||
const wallet = wallets.find(w => w.getID() === data.userInfo.url.split('wallet/')[1]);
|
||||
if (wallet) {
|
||||
NavigationService.dispatch(
|
||||
CommonActions.navigate({
|
||||
@ -126,7 +125,7 @@ function DeviceQuickActions() {
|
||||
};
|
||||
|
||||
const walletQuickActions = (data: any): void => {
|
||||
const wallet = wallets.find((w: { getID: () => any }) => w.getID() === data.userInfo.url.split('wallet/')[1]);
|
||||
const wallet = wallets.find(w => w.getID() === data.userInfo.url.split('wallet/')[1]);
|
||||
if (wallet) {
|
||||
NavigationService.dispatch(
|
||||
CommonActions.navigate({
|
||||
@ -153,22 +152,21 @@ function DeviceQuickActions() {
|
||||
if (await getEnabled()) {
|
||||
QuickActions.isSupported((error: null, _supported: any) => {
|
||||
if (error === null) {
|
||||
const shortcutItems = [];
|
||||
for (const wallet of wallets.slice(0, 4)) {
|
||||
shortcutItems.push({
|
||||
type: 'Wallets', // Required
|
||||
title: wallet.getLabel(), // Optional, if empty, `type` will be used instead
|
||||
subtitle:
|
||||
wallet.hideBalance || wallet.getBalance() <= 0
|
||||
? ''
|
||||
: formatBalance(Number(wallet.getBalance()), wallet.getPreferredBalanceUnit(), true),
|
||||
userInfo: {
|
||||
url: `bluewallet://wallet/${wallet.getID()}`, // Provide any custom data like deep linking URL
|
||||
},
|
||||
icon: Platform.select({ android: 'quickactions', ios: 'bookmark' }),
|
||||
});
|
||||
}
|
||||
// @ts-ignore: Fix later
|
||||
const shortcutItems: ShortcutItem[] = wallets.slice(0, 4).map((wallet, index) => ({
|
||||
type: 'Wallets',
|
||||
title: wallet.getLabel(),
|
||||
subtitle:
|
||||
wallet.hideBalance || wallet.getBalance() <= 0
|
||||
? ''
|
||||
: formatBalance(Number(wallet.getBalance()), wallet.getPreferredBalanceUnit(), true),
|
||||
userInfo: {
|
||||
url: `bluewallet://wallet/${wallet.getID()}`,
|
||||
},
|
||||
icon: Platform.select({
|
||||
android: 'quickactions',
|
||||
ios: index === 0 ? 'Favorite' : 'Bookmark',
|
||||
}) || 'quickactions',
|
||||
}));
|
||||
QuickActions.setShortcutItems(shortcutItems);
|
||||
}
|
||||
});
|
||||
@ -177,7 +175,7 @@ function DeviceQuickActions() {
|
||||
}
|
||||
};
|
||||
|
||||
return null;
|
||||
return { popInitialAction };
|
||||
}
|
||||
|
||||
export default DeviceQuickActions;
|
||||
export default useDeviceQuickActions;
|
17
hooks/useDeviceQuickActions.windows.ts
Normal file
17
hooks/useDeviceQuickActions.windows.ts
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
export const DeviceQuickActionsStorageKey = 'DeviceQuickActionsEnabled';
|
||||
|
||||
export const setEnabled = (): void => {};
|
||||
|
||||
export const getEnabled = async (): Promise<boolean> => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const useDeviceQuickActions = () => {
|
||||
|
||||
const popInitialAction = (): void => {};
|
||||
return { popInitialAction };
|
||||
};
|
||||
|
||||
|
||||
export default useDeviceQuickActions;
|
63
hooks/useHandoffListener.ios.ts
Normal file
63
hooks/useHandoffListener.ios.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { NativeEventEmitter, NativeModules } from 'react-native';
|
||||
import { useStorage } from '../hooks/context/useStorage';
|
||||
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
|
||||
import { HandOffActivityType } from '../components/types';
|
||||
import { useSettings } from './context/useSettings';
|
||||
|
||||
interface UserActivityData {
|
||||
activityType: HandOffActivityType;
|
||||
userInfo: {
|
||||
address?: string;
|
||||
xpub?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const EventEmitter = NativeModules.EventEmitter;
|
||||
const eventEmitter = EventEmitter ? new NativeEventEmitter(EventEmitter) : null;
|
||||
|
||||
const useHandoffListener = () => {
|
||||
const { walletsInitialized } = useStorage();
|
||||
const { isHandOffUseEnabled } = useSettings();
|
||||
const { navigate } = useExtendedNavigation();
|
||||
|
||||
const handleUserActivity = useCallback(
|
||||
(data: UserActivityData) => {
|
||||
const { activityType, userInfo } = data;
|
||||
try {
|
||||
if (activityType === HandOffActivityType.ReceiveOnchain) {
|
||||
navigate('ReceiveDetailsRoot', {
|
||||
screen: 'ReceiveDetails',
|
||||
params: { address: userInfo.address },
|
||||
});
|
||||
} else if (activityType === HandOffActivityType.Xpub) {
|
||||
navigate('WalletXpubRoot', {
|
||||
screen: 'WalletXpub',
|
||||
params: { xpub: userInfo.xpub },
|
||||
});
|
||||
} else {
|
||||
console.debug(`Unhandled activity type: ${activityType}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling user activity:', error);
|
||||
}
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!walletsInitialized || !isHandOffUseEnabled) return;
|
||||
|
||||
const activitySubscription = eventEmitter?.addListener('onUserActivityOpen', handleUserActivity);
|
||||
|
||||
EventEmitter.getMostRecentUserActivity?.()
|
||||
.then(handleUserActivity)
|
||||
.catch(() => console.debug('No userActivity object sent'));
|
||||
|
||||
return () => {
|
||||
activitySubscription?.remove();
|
||||
};
|
||||
}, [walletsInitialized, isHandOffUseEnabled, handleUserActivity]);
|
||||
};
|
||||
|
||||
export default useHandoffListener;
|
3
hooks/useHandoffListener.ts
Normal file
3
hooks/useHandoffListener.ts
Normal file
@ -0,0 +1,3 @@
|
||||
const useHandoffListener = () => {};
|
||||
|
||||
export default useHandoffListener;
|
@ -1592,7 +1592,7 @@ PODS:
|
||||
- React-Core
|
||||
- RealmJS (20.0.0):
|
||||
- React
|
||||
- RNCAsyncStorage (2.0.0):
|
||||
- RNCAsyncStorage (2.1.0):
|
||||
- React-Core
|
||||
- RNCClipboard (1.15.0):
|
||||
- React-Core
|
||||
@ -1810,7 +1810,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNSVG (15.8.0):
|
||||
- RNSVG (15.9.0):
|
||||
- React-Core
|
||||
- RNVectorIcons (10.2.0):
|
||||
- DoubleConversion
|
||||
@ -2260,7 +2260,7 @@ SPEC CHECKSUMS:
|
||||
ReactCommon: 36d48f542b4010786d6b2bcee615fe5f906b7105
|
||||
ReactNativeCameraKit: f058d47e0b1e55fd819bb55ee16505a2e0ca53db
|
||||
RealmJS: 7947e9d9edcfb4fde3dcf9911d320a2e08cdd540
|
||||
RNCAsyncStorage: 40367e8d25522dca9c3513c7b9815a184669bd97
|
||||
RNCAsyncStorage: c91d753ede6dc21862c4922cd13f98f7cfde578e
|
||||
RNCClipboard: dbcf25b8f666b4685c02eeb65be981d30198e505
|
||||
RNCPushNotificationIOS: 6c4ca3388c7434e4a662b92e4dfeeee858e6f440
|
||||
RNDefaultPreference: ee13d69e6693d193cd223d10e15e5b3c012d31ba
|
||||
@ -2278,7 +2278,7 @@ SPEC CHECKSUMS:
|
||||
RNReanimated: 6398ee150e1ebeda517fdd1e1b5525833a0c0ddc
|
||||
RNScreens: 35bb8e81aeccf111baa0ea01a54231390dbbcfd9
|
||||
RNShare: 6af59763338a7d8440035701f39be9d53cbc4d09
|
||||
RNSVG: 8542aa11770b27563714bbd8494a8436385fc85f
|
||||
RNSVG: bb4bfcb8ec723a6f34b074a1b7cd40ee35246fe5
|
||||
RNVectorIcons: 182892e7d1a2f27b52d3c627eca5d2665a22ee28
|
||||
RNWatch: 28fe1f5e0c6410d45fd20925f4796fce05522e3f
|
||||
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
|
||||
|
@ -14,60 +14,59 @@ struct APIError: LocalizedError {
|
||||
|
||||
extension MarketAPI {
|
||||
|
||||
static func fetchNextBlockFee(completion: @escaping ((MarketData?, Error?) -> Void), userElectrumSettings: UserDefaultsElectrumSettings = UserDefaultsGroup.getElectrumSettings()) {
|
||||
let settings = userElectrumSettings
|
||||
let portToUse = settings.sslPort ?? settings.port
|
||||
let isSSLSupported = settings.sslPort != nil
|
||||
static func fetchNextBlockFee(completion: @escaping ((MarketData?, Error?) -> Void), userElectrumSettings: UserDefaultsElectrumSettings = UserDefaultsGroup.getElectrumSettings()) {
|
||||
Task {
|
||||
let client = SwiftTCPClient()
|
||||
defer {
|
||||
print("Closing connection to \(userElectrumSettings.host ?? "unknown"):\(userElectrumSettings.sslPort ?? userElectrumSettings.port ?? 0).")
|
||||
client.close()
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let client = SwiftTCPClient()
|
||||
guard let host = userElectrumSettings.host, let portToUse = userElectrumSettings.sslPort ?? userElectrumSettings.port else {
|
||||
completion(nil, APIError())
|
||||
return
|
||||
}
|
||||
|
||||
defer {
|
||||
print("Closing connection to \(String(describing: settings.host)):\(String(describing: portToUse)).")
|
||||
client.close()
|
||||
}
|
||||
let isSSLSupported = userElectrumSettings.sslPort != nil
|
||||
print("Attempting to connect to \(host):\(portToUse) with SSL supported: \(isSSLSupported).")
|
||||
|
||||
guard let host = settings.host, let portToUse = portToUse else { return }
|
||||
let connected = await client.connect(to: host, port: portToUse, useSSL: isSSLSupported)
|
||||
if connected {
|
||||
print("Successfully connected to \(host):\(portToUse) with SSL: \(isSSLSupported).")
|
||||
} else {
|
||||
print("Failed to connect to \(host):\(portToUse) with SSL: \(isSSLSupported).")
|
||||
completion(nil, APIError())
|
||||
return
|
||||
}
|
||||
|
||||
print("Attempting to connect to \(String(describing: settings.host)):\(portToUse) with SSL supported: \(isSSLSupported).")
|
||||
let message = "{\"id\": 1, \"method\": \"mempool.get_fee_histogram\", \"params\": []}\n"
|
||||
guard let data = message.data(using: .utf8), await client.send(data: data) else {
|
||||
print("Message sending failed to \(host):\(portToUse) with SSL supported: \(isSSLSupported).")
|
||||
completion(nil, APIError())
|
||||
return
|
||||
}
|
||||
print("Message sent successfully to \(host):\(portToUse) with SSL: \(isSSLSupported).")
|
||||
|
||||
if client.connect(to: host, port: UInt32(portToUse), useSSL: isSSLSupported) {
|
||||
print("Successfully connected to \(String(describing: settings.host)):\(portToUse) with SSL:\(isSSLSupported).")
|
||||
} else {
|
||||
print("Failed to connect to \(String(describing: settings.host)):\(portToUse) with SSL:\(isSSLSupported).")
|
||||
completion(nil, APIError())
|
||||
return
|
||||
}
|
||||
do {
|
||||
let receivedData = try await client.receive()
|
||||
print("Data received. Parsing...")
|
||||
guard let json = try JSONSerialization.jsonObject(with: receivedData, options: .allowFragments) as? [String: AnyObject],
|
||||
let feeHistogram = json["result"] as? [[Double]] else {
|
||||
print("Failed to parse response from \(host).")
|
||||
completion(nil, APIError())
|
||||
return
|
||||
}
|
||||
|
||||
let message = "{\"id\": 1, \"method\": \"mempool.get_fee_histogram\", \"params\": []}\n"
|
||||
guard let data = message.data(using: .utf8), client.send(data: data) else {
|
||||
print("Message sending failed to \(String(describing: settings.host)):\(portToUse) with SSL supported: \(isSSLSupported).")
|
||||
completion(nil, APIError())
|
||||
return
|
||||
}
|
||||
print("Message sent successfully to \(String(describing: settings.host)):\(portToUse) with SSL:\(isSSLSupported).")
|
||||
|
||||
do {
|
||||
let receivedData = try client.receive()
|
||||
print("Data received. Parsing...")
|
||||
guard let responseString = String(data: receivedData, encoding: .utf8),
|
||||
let responseData = responseString.data(using: .utf8),
|
||||
let json = try JSONSerialization.jsonObject(with: responseData, options: .allowFragments) as? [String: AnyObject],
|
||||
let feeHistogram = json["result"] as? [[Double]] else {
|
||||
print("Failed to parse response from \(String(describing: settings.host)).")
|
||||
completion(nil, APIError())
|
||||
return
|
||||
}
|
||||
|
||||
let fastestFee = calcEstimateFeeFromFeeHistogram(numberOfBlocks: 1, feeHistogram: feeHistogram)
|
||||
let marketData = MarketData(nextBlock: String(format: "%.0f", fastestFee), sats: "0", price: "0", rate: 0)
|
||||
completion(marketData, nil) // Successfully fetched data, return it
|
||||
} catch {
|
||||
print("Error receiving data from \(String(describing: settings.host)): \(error.localizedDescription)")
|
||||
completion(nil, APIError())
|
||||
}
|
||||
}
|
||||
}
|
||||
let fastestFee = calcEstimateFeeFromFeeHistogram(numberOfBlocks: 1, feeHistogram: feeHistogram)
|
||||
let marketData = MarketData(nextBlock: String(format: "%.0f", fastestFee), sats: "0", price: "0", rate: 0, dateString: "")
|
||||
print("Parsed MarketData: \(marketData)")
|
||||
completion(marketData, nil)
|
||||
} catch {
|
||||
print("Error receiving data from \(host): \(error.localizedDescription)")
|
||||
completion(nil, APIError())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func fetchMarketData(currency: String, completion: @escaping ((MarketData?, Error?) -> Void)) {
|
||||
var marketDataEntry = MarketData(nextBlock: "...", sats: "...", price: "...", rate: 0)
|
||||
|
@ -9,9 +9,9 @@
|
||||
import Foundation
|
||||
|
||||
struct UserDefaultsElectrumSettings {
|
||||
let host: String?
|
||||
let port: Int32?
|
||||
let sslPort: Int32?
|
||||
var host: String?
|
||||
var port: UInt16?
|
||||
var sslPort: UInt16?
|
||||
}
|
||||
|
||||
let hardcodedPeers = [
|
||||
@ -34,14 +34,14 @@ class UserDefaultsGroup {
|
||||
return DefaultElectrumPeers.randomElement()!
|
||||
}
|
||||
|
||||
let electrumSettingsTCPPort = suite?.string(forKey: UserDefaultsGroupKey.ElectrumSettingsTCPPort.rawValue) ?? "50001"
|
||||
let electrumSettingsSSLPort = suite?.string(forKey: UserDefaultsGroupKey.ElectrumSettingsSSLPort.rawValue) ?? "443"
|
||||
let electrumSettingsTCPPort = suite?.value(forKey: UserDefaultsGroupKey.ElectrumSettingsTCPPort.rawValue) ?? 50001
|
||||
let electrumSettingsSSLPort = suite?.value(forKey: UserDefaultsGroupKey.ElectrumSettingsSSLPort.rawValue) ?? 443
|
||||
|
||||
let host = electrumSettingsHost
|
||||
let sslPort = Int32(electrumSettingsSSLPort)
|
||||
let port = Int32(electrumSettingsTCPPort)
|
||||
let sslPort = electrumSettingsSSLPort
|
||||
let port = electrumSettingsTCPPort
|
||||
|
||||
return UserDefaultsElectrumSettings(host: host, port: port, sslPort: sslPort)
|
||||
return UserDefaultsElectrumSettings(host: host, port: port as! UInt16, sslPort: sslPort as! UInt16)
|
||||
}
|
||||
|
||||
static func getAllWalletsBalance() -> Double {
|
||||
|
@ -1,152 +1,156 @@
|
||||
// BlueWallet
|
||||
//
|
||||
// Created by Marcos Rodriguez on 3/23/23.
|
||||
// Copyright © 2023 BlueWallet. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
/**
|
||||
`SwiftTCPClient` is a simple TCP client class that allows for establishing a TCP connection,
|
||||
sending data, and receiving data over the network. It supports both plain TCP and SSL-secured connections.
|
||||
|
||||
The class uses `InputStream` and `OutputStream` for network communication, encapsulating the complexity of stream management and data transfer.
|
||||
|
||||
- Note: When using SSL, this implementation disables certificate chain validation for simplicity. This is not recommended for production code due to security risks.
|
||||
|
||||
## Examples
|
||||
|
||||
### Creating an instance and connecting to a server:
|
||||
|
||||
```swift
|
||||
let client = SwiftTCPClient()
|
||||
let success = client.connect(to: "example.com", port: 12345, useSSL: false)
|
||||
|
||||
if success {
|
||||
print("Connected successfully.")
|
||||
} else {
|
||||
print("Failed to connect.")
|
||||
}
|
||||
**/
|
||||
|
||||
class SwiftTCPClient: NSObject {
|
||||
private var inputStream: InputStream?
|
||||
private var outputStream: OutputStream?
|
||||
private let bufferSize = 1024
|
||||
private var readData = Data()
|
||||
private let readTimeout = 5.0 // Timeout in seconds
|
||||
|
||||
func connect(to host: String, port: UInt32, useSSL: Bool = false) -> Bool {
|
||||
var readStream: Unmanaged<CFReadStream>?
|
||||
var writeStream: Unmanaged<CFWriteStream>?
|
||||
|
||||
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, host as CFString, port, &readStream, &writeStream)
|
||||
|
||||
guard let read = readStream?.takeRetainedValue(), let write = writeStream?.takeRetainedValue() else {
|
||||
return false
|
||||
enum SwiftTCPClientError: Error, LocalizedError {
|
||||
case connectionNil
|
||||
case connectionCancelled
|
||||
case readTimedOut
|
||||
case noDataReceived
|
||||
case unknown(Error)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .connectionNil:
|
||||
return "Connection is nil."
|
||||
case .connectionCancelled:
|
||||
return "Connection was cancelled."
|
||||
case .readTimedOut:
|
||||
return "Read timed out."
|
||||
case .noDataReceived:
|
||||
return "No data received."
|
||||
case .unknown(let error):
|
||||
return error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inputStream = read as InputStream
|
||||
outputStream = write as OutputStream
|
||||
class SwiftTCPClient {
|
||||
private var connection: NWConnection?
|
||||
private let queue = DispatchQueue(label: "SwiftTCPClientQueue")
|
||||
private let readTimeout: TimeInterval = 5.0
|
||||
|
||||
func connect(to host: String, port: UInt16, useSSL: Bool = false) async -> Bool {
|
||||
let parameters: NWParameters
|
||||
if useSSL {
|
||||
// Configure SSL settings for the streams
|
||||
let sslSettings: [NSString: Any] = [
|
||||
kCFStreamSSLLevel as NSString: kCFStreamSocketSecurityLevelNegotiatedSSL as Any,
|
||||
kCFStreamSSLValidatesCertificateChain as NSString: kCFBooleanFalse as Any
|
||||
// Note: Disabling certificate chain validation (kCFStreamSSLValidatesCertificateChain: kCFBooleanFalse)
|
||||
// is typically not recommended for production code as it introduces significant security risks.
|
||||
]
|
||||
inputStream?.setProperty(sslSettings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey)
|
||||
outputStream?.setProperty(sslSettings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey)
|
||||
parameters = NWParameters(tls: createTLSOptions(), tcp: .init())
|
||||
} else {
|
||||
parameters = NWParameters.tcp
|
||||
}
|
||||
|
||||
inputStream?.delegate = self
|
||||
outputStream?.delegate = self
|
||||
|
||||
inputStream?.schedule(in: .current, forMode: RunLoop.Mode.default)
|
||||
outputStream?.schedule(in: .current, forMode: RunLoop.Mode.default)
|
||||
|
||||
inputStream?.open()
|
||||
outputStream?.open()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
func send(data: Data) -> Bool {
|
||||
guard let outputStream = outputStream else {
|
||||
guard let nwPort = NWEndpoint.Port(rawValue: port) else {
|
||||
print("Invalid port number: \(port)")
|
||||
return false
|
||||
}
|
||||
connection = NWConnection(host: NWEndpoint.Host(host), port: nwPort, using: parameters)
|
||||
connection?.start(queue: queue)
|
||||
|
||||
let bytesWritten = data.withUnsafeBytes { bufferPointer -> Int in
|
||||
guard let baseAddress = bufferPointer.baseAddress else {
|
||||
return 0
|
||||
}
|
||||
return outputStream.write(baseAddress.assumingMemoryBound(to: UInt8.self), maxLength: data.count)
|
||||
}
|
||||
let serialQueue = DispatchQueue(label: "SwiftTCPClient.connect.serialQueue")
|
||||
var hasResumed = false
|
||||
|
||||
return bytesWritten == data.count
|
||||
}
|
||||
|
||||
func receive() throws -> Data {
|
||||
guard let inputStream = inputStream else {
|
||||
throw NSError(domain: "SwiftTCPClientError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Input stream is nil."])
|
||||
}
|
||||
|
||||
// Check if the input stream is ready for reading
|
||||
if inputStream.streamStatus != .open && inputStream.streamStatus != .reading {
|
||||
throw NSError(domain: "SwiftTCPClientError", code: 3, userInfo: [NSLocalizedDescriptionKey: "Stream is not ready for reading."])
|
||||
}
|
||||
|
||||
readData = Data()
|
||||
|
||||
// Wait for data to be available or timeout
|
||||
let timeoutDate = Date().addingTimeInterval(readTimeout)
|
||||
repeat {
|
||||
RunLoop.current.run(mode: RunLoop.Mode.default, before: Date(timeIntervalSinceNow: 0.1))
|
||||
if readData.count > 0 || Date() > timeoutDate {
|
||||
break
|
||||
}
|
||||
} while inputStream.streamStatus == .open || inputStream.streamStatus == .reading
|
||||
|
||||
if readData.count == 0 && Date() > timeoutDate {
|
||||
throw NSError(domain: "SwiftTCPClientError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Read timed out."])
|
||||
}
|
||||
|
||||
return readData
|
||||
}
|
||||
|
||||
|
||||
func close() {
|
||||
inputStream?.close()
|
||||
outputStream?.close()
|
||||
inputStream?.remove(from: .current, forMode: RunLoop.Mode.default)
|
||||
outputStream?.remove(from: .current, forMode: RunLoop.Mode.default)
|
||||
inputStream = nil
|
||||
outputStream = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension SwiftTCPClient: StreamDelegate {
|
||||
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
|
||||
switch eventCode {
|
||||
case .hasBytesAvailable:
|
||||
if aStream == inputStream, let inputStream = inputStream {
|
||||
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
||||
while inputStream.hasBytesAvailable {
|
||||
let bytesRead = inputStream.read(buffer, maxLength: bufferSize)
|
||||
if bytesRead > 0 {
|
||||
readData.append(buffer, count: bytesRead)
|
||||
do {
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
connection?.stateUpdateHandler = { [weak self] state in
|
||||
guard let self = self else { return }
|
||||
serialQueue.async {
|
||||
if !hasResumed {
|
||||
switch state {
|
||||
case .ready:
|
||||
self.connection?.stateUpdateHandler = nil
|
||||
hasResumed = true
|
||||
continuation.resume()
|
||||
case .failed(let error):
|
||||
self.connection?.stateUpdateHandler = nil
|
||||
hasResumed = true
|
||||
continuation.resume(throwing: error)
|
||||
case .cancelled:
|
||||
self.connection?.stateUpdateHandler = nil
|
||||
hasResumed = true
|
||||
continuation.resume(throwing: SwiftTCPClientError.connectionCancelled)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer.deallocate()
|
||||
}
|
||||
case .errorOccurred:
|
||||
print("Stream error occurred")
|
||||
case .endEncountered:
|
||||
close()
|
||||
default:
|
||||
break
|
||||
return true
|
||||
} catch {
|
||||
print("Connection failed with error: \(error.localizedDescription)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func send(data: Data) async -> Bool {
|
||||
guard let connection = connection else {
|
||||
print("Send failed: No active connection.")
|
||||
return false
|
||||
}
|
||||
|
||||
do {
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
connection.send(content: data, completion: .contentProcessed { error in
|
||||
if let error = error {
|
||||
print("Send error: \(error.localizedDescription)")
|
||||
continuation.resume(throwing: error)
|
||||
} else {
|
||||
continuation.resume()
|
||||
}
|
||||
})
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
print("Send failed with error: \(error.localizedDescription)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func receive() async throws -> Data {
|
||||
guard let connection = connection else {
|
||||
throw SwiftTCPClientError.connectionNil
|
||||
}
|
||||
|
||||
return try await withThrowingTaskGroup(of: Data.self) { group in
|
||||
group.addTask {
|
||||
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Data, Error>) in
|
||||
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, isComplete, error in
|
||||
if let error = error {
|
||||
continuation.resume(throwing: SwiftTCPClientError.unknown(error))
|
||||
return
|
||||
}
|
||||
|
||||
if let data = data, !data.isEmpty {
|
||||
continuation.resume(returning: data)
|
||||
} else if isComplete {
|
||||
self.close()
|
||||
continuation.resume(throwing: SwiftTCPClientError.noDataReceived)
|
||||
} else {
|
||||
continuation.resume(throwing: SwiftTCPClientError.readTimedOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.addTask {
|
||||
try await Task.sleep(nanoseconds: UInt64(self.readTimeout * 1_000_000_000))
|
||||
throw SwiftTCPClientError.readTimedOut
|
||||
}
|
||||
|
||||
if let firstResult = try await group.next() {
|
||||
group.cancelAll()
|
||||
return firstResult
|
||||
} else {
|
||||
throw SwiftTCPClientError.readTimedOut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func close() {
|
||||
connection?.cancel()
|
||||
connection = nil
|
||||
}
|
||||
|
||||
private func createTLSOptions() -> NWProtocolTLS.Options {
|
||||
let tlsOptions = NWProtocolTLS.Options()
|
||||
return tlsOptions
|
||||
}
|
||||
}
|
||||
|
@ -597,10 +597,11 @@
|
||||
"tip": "This feature allows you to see, label, freeze or select coins for improved wallet management. You can select multiple coins by tapping on the colored circles.",
|
||||
"sort_asc": "Ascending",
|
||||
"sort_desc": "Descending",
|
||||
"sort_height": "by Height",
|
||||
"sort_value": "by Value",
|
||||
"sort_label": "by Label",
|
||||
"sort_status": "by Status"
|
||||
"sort_height": "Height",
|
||||
"sort_value": "Value",
|
||||
"sort_label": "Label",
|
||||
"sort_status": "Status",
|
||||
"sort_by": "Sort by"
|
||||
},
|
||||
"units": {
|
||||
"BTC": "BTC",
|
||||
|
@ -320,7 +320,8 @@
|
||||
"would_you_like_to_receive_notifications": "¿Te gustaría recibir notificaciones cuando recibas pagos entrantes?",
|
||||
"notifications_subtitle": "Pagos entrantes y confirmaciones de transacciones",
|
||||
"no_and_dont_ask": "No, y no me vuelvas a preguntar.",
|
||||
"ask_me_later": "Pregúntame Luego."
|
||||
"ask_me_later": "Pregúntame Luego.",
|
||||
"permission_denied_message": "Has denegado el envío de notificaciones. Si deseas recibirlas, actívalas en la configuración de tu dispositivo."
|
||||
},
|
||||
"transactions": {
|
||||
"cancel_explain": "Reemplazaremos esta transacción con una que te pague y tenga tarifas más altas. Esto cancela efectivamente la transacción actual. Esto se llama RBF—Replace by Fee.",
|
||||
@ -447,6 +448,7 @@
|
||||
"import_discovery_subtitle": "Elige una billetera descubierta",
|
||||
"import_discovery_derivation": "Utilizar una ruta de derivación personalizada",
|
||||
"import_discovery_no_wallets": "No se encontraron billeteras.",
|
||||
"import_discovery_offline": "BlueWallet se encuentra actualmente en modo sin conexión. En este modo, no puede verificar la existencia de la billetera, por lo que deberás seleccionar la correcta manualmente.",
|
||||
"import_derivation_found": "Encontrado",
|
||||
"import_derivation_found_not": "No se ha encontrado",
|
||||
"import_derivation_loading": "Cargando...",
|
||||
@ -595,10 +597,11 @@
|
||||
"tip": "Esta función te permite ver, etiquetar, congelar o seleccionar monedas para una mejor gestión de la cartera. Puedes seleccionar varias monedas tocando los círculos de colores.",
|
||||
"sort_asc": "Ascendente",
|
||||
"sort_desc": "Descendente",
|
||||
"sort_height": "por Altura",
|
||||
"sort_value": "por Valor",
|
||||
"sort_label": "por Etiqueta",
|
||||
"sort_status": "por Estado"
|
||||
"sort_height": "Altura",
|
||||
"sort_value": "Valor",
|
||||
"sort_label": "Etiqueta",
|
||||
"sort_status": "Estado",
|
||||
"sort_by": "Ordenar por"
|
||||
},
|
||||
"units": {
|
||||
"BTC": "BTC",
|
||||
|
40
package-lock.json
generated
40
package-lock.json
generated
@ -17,7 +17,7 @@
|
||||
"@lodev09/react-native-true-sheet": "github:BlueWallet/react-native-true-sheet#839f2966cee77c0ad99d09609dadb61a338e7f54",
|
||||
"@ngraveio/bc-ur": "1.1.13",
|
||||
"@noble/secp256k1": "1.6.3",
|
||||
"@react-native-async-storage/async-storage": "2.0.0",
|
||||
"@react-native-async-storage/async-storage": "2.1.0",
|
||||
"@react-native-clipboard/clipboard": "1.15.0",
|
||||
"@react-native-community/push-notification-ios": "1.11.0",
|
||||
"@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#02592ae",
|
||||
@ -46,7 +46,7 @@
|
||||
"coinselect": "3.1.13",
|
||||
"crypto-js": "4.2.0",
|
||||
"dayjs": "1.11.13",
|
||||
"detox": "20.27.6",
|
||||
"detox": "20.28.0",
|
||||
"ecpair": "2.0.1",
|
||||
"ecurve": "1.0.6",
|
||||
"electrum-client": "github:BlueWallet/rn-electrum-client#1bfe3cc",
|
||||
@ -70,7 +70,7 @@
|
||||
"react-native-document-picker": "9.3.1",
|
||||
"react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#3a61627",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-gesture-handler": "2.20.2",
|
||||
"react-native-gesture-handler": "2.21.1",
|
||||
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
|
||||
"react-native-haptic-feedback": "2.3.3",
|
||||
"react-native-image-picker": "7.1.2",
|
||||
@ -91,7 +91,7 @@
|
||||
"react-native-screens": "3.35.0",
|
||||
"react-native-secure-key-store": "github:BlueWallet/react-native-secure-key-store#2076b4849e88aa0a78e08bfbb4ce3923e0925cbc",
|
||||
"react-native-share": "11.0.4",
|
||||
"react-native-svg": "15.8.0",
|
||||
"react-native-svg": "15.9.0",
|
||||
"react-native-tcp-socket": "6.2.0",
|
||||
"react-native-vector-icons": "10.2.0",
|
||||
"react-native-watch-connectivity": "1.1.0",
|
||||
@ -4507,9 +4507,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-async-storage/async-storage": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.0.0.tgz",
|
||||
"integrity": "sha512-af6H9JjfL6G/PktBfUivvexoiFKQTJGQCtSWxMdivLzNIY94mu9DdiY0JqCSg/LyPCLGKhHPUlRQhNvpu3/KVA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.1.0.tgz",
|
||||
"integrity": "sha512-eAGQGPTAuFNEoIQSB5j2Jh1zm5NPyBRTfjRMfCN0W1OakC5WIB5vsDyIQhUweKN9XOE2/V07lqTMGsL0dGXNkA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"merge-options": "^3.0.4"
|
||||
@ -9731,9 +9731,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/detox": {
|
||||
"version": "20.27.6",
|
||||
"resolved": "https://registry.npmjs.org/detox/-/detox-20.27.6.tgz",
|
||||
"integrity": "sha512-eyVkBC7uFpxgUAuWjvbejwnPOjRHqUWSSE/P0FwsCxWVnrNOSq8IhBnSj82Ic5Dc76CAB+Xw5SWYsHnbhG26Bg==",
|
||||
"version": "20.28.0",
|
||||
"resolved": "https://registry.npmjs.org/detox/-/detox-20.28.0.tgz",
|
||||
"integrity": "sha512-JeUkWNnYE7lqby3S9AeYJP3ttCBKH+qZWACjWXwvSbe3tm6JeXvecVUYkzSoNfC4IzTX5p+rWvG0IPsfOsZSFw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -9743,7 +9743,7 @@
|
||||
"caf": "^15.0.1",
|
||||
"chalk": "^4.0.0",
|
||||
"child-process-promise": "^2.2.0",
|
||||
"detox-copilot": "^0.0.23",
|
||||
"detox-copilot": "^0.0.24",
|
||||
"execa": "^5.1.1",
|
||||
"find-up": "^5.0.0",
|
||||
"fs-extra": "^11.0.0",
|
||||
@ -9790,9 +9790,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/detox-copilot": {
|
||||
"version": "0.0.23",
|
||||
"resolved": "https://registry.npmjs.org/detox-copilot/-/detox-copilot-0.0.23.tgz",
|
||||
"integrity": "sha512-qDSdLwgPUMVawpE0R3agNWd2U69ilTnhf+SodSqqrkmTI0oG67IfkACvwox+K9Slcc8ki6y0Bw6QVBi54MqpaA==",
|
||||
"version": "0.0.24",
|
||||
"resolved": "https://registry.npmjs.org/detox-copilot/-/detox-copilot-0.0.24.tgz",
|
||||
"integrity": "sha512-42g0QyJS31URl28YRxc4hGozSXhbbB1sKwzxEjZR9WtLoSx6WYDsQkQD8+yP5t1NExiSCZAfvNmBw8PYQwDKwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detox/node_modules/ansi-styles": {
|
||||
@ -20648,9 +20648,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-gesture-handler": {
|
||||
"version": "2.20.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",
|
||||
"integrity": "sha512-HqzFpFczV4qCnwKlvSAvpzEXisL+Z9fsR08YV5LfJDkzuArMhBu2sOoSPUF/K62PCoAb+ObGlTC83TKHfUd0vg==",
|
||||
"version": "2.21.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.21.1.tgz",
|
||||
"integrity": "sha512-KT19IvAP2nto/9WhDlZ6u62utOvi72VhR1PgqB3zDIiBdRDCPrLCYRduxfz6q8VuCIyzWAcuOh8vas7zmO5Y1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@egjs/hammerjs": "^2.0.17",
|
||||
@ -20928,9 +20928,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-svg": {
|
||||
"version": "15.8.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.8.0.tgz",
|
||||
"integrity": "sha512-KHJzKpgOjwj1qeZzsBjxNdoIgv2zNCO9fVcoq2TEhTRsVV5DGTZ9JzUZwybd7q4giT/H3RdtqC3u44dWdO0Ffw==",
|
||||
"version": "15.9.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.9.0.tgz",
|
||||
"integrity": "sha512-pwo7hteAM0P8jNpPGQtiSd0SnbBhE8tNd94LT8AcZcbnH5AJdXBIcXU4+tWYYeGUjiNAH2E5d0T5XIfnvaz1gA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-select": "^5.1.0",
|
||||
|
@ -81,7 +81,7 @@
|
||||
"@lodev09/react-native-true-sheet": "github:BlueWallet/react-native-true-sheet#839f2966cee77c0ad99d09609dadb61a338e7f54",
|
||||
"@ngraveio/bc-ur": "1.1.13",
|
||||
"@noble/secp256k1": "1.6.3",
|
||||
"@react-native-async-storage/async-storage": "2.0.0",
|
||||
"@react-native-async-storage/async-storage": "2.1.0",
|
||||
"@react-native-clipboard/clipboard": "1.15.0",
|
||||
"@react-native-community/push-notification-ios": "1.11.0",
|
||||
"@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#02592ae",
|
||||
@ -110,7 +110,7 @@
|
||||
"coinselect": "3.1.13",
|
||||
"crypto-js": "4.2.0",
|
||||
"dayjs": "1.11.13",
|
||||
"detox": "20.27.6",
|
||||
"detox": "20.28.0",
|
||||
"ecpair": "2.0.1",
|
||||
"ecurve": "1.0.6",
|
||||
"electrum-client": "github:BlueWallet/rn-electrum-client#1bfe3cc",
|
||||
@ -134,7 +134,7 @@
|
||||
"react-native-document-picker": "9.3.1",
|
||||
"react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#3a61627",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-gesture-handler": "2.20.2",
|
||||
"react-native-gesture-handler": "2.21.1",
|
||||
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
|
||||
"react-native-haptic-feedback": "2.3.3",
|
||||
"react-native-image-picker": "7.1.2",
|
||||
@ -155,7 +155,7 @@
|
||||
"react-native-screens": "3.35.0",
|
||||
"react-native-secure-key-store": "github:BlueWallet/react-native-secure-key-store#2076b4849e88aa0a78e08bfbb4ce3923e0925cbc",
|
||||
"react-native-share": "11.0.4",
|
||||
"react-native-svg": "15.8.0",
|
||||
"react-native-svg": "15.9.0",
|
||||
"react-native-tcp-socket": "6.2.0",
|
||||
"react-native-vector-icons": "10.2.0",
|
||||
"react-native-watch-connectivity": "1.1.0",
|
||||
|
@ -457,13 +457,15 @@ const CoinControl: React.FC = () => {
|
||||
}
|
||||
}, [output]);
|
||||
|
||||
const toolTipActions = useMemo((): Action[] => {
|
||||
const toolTipActions = useMemo((): Action[] | Action[][] => {
|
||||
return [
|
||||
sortDirection === ESortDirections.asc ? CommonToolTipActions.SortASC : CommonToolTipActions.SortDESC,
|
||||
{ ...CommonToolTipActions.SortHeight, menuState: sortType === ESortTypes.height },
|
||||
{ ...CommonToolTipActions.SortValue, menuState: sortType === ESortTypes.value },
|
||||
{ ...CommonToolTipActions.SortLabel, menuState: sortType === ESortTypes.label },
|
||||
{ ...CommonToolTipActions.SortStatus, menuState: sortType === ESortTypes.frozen },
|
||||
[sortDirection === ESortDirections.asc ? CommonToolTipActions.SortASC : CommonToolTipActions.SortDESC],
|
||||
[
|
||||
{ ...CommonToolTipActions.SortHeight, menuState: sortType === ESortTypes.height },
|
||||
{ ...CommonToolTipActions.SortValue, menuState: sortType === ESortTypes.value },
|
||||
{ ...CommonToolTipActions.SortLabel, menuState: sortType === ESortTypes.label },
|
||||
{ ...CommonToolTipActions.SortStatus, menuState: sortType === ESortTypes.frozen },
|
||||
],
|
||||
];
|
||||
}, [sortDirection, sortType]);
|
||||
|
||||
@ -485,7 +487,7 @@ const CoinControl: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
const HeaderRight = useMemo(
|
||||
() => <HeaderMenuButton onPressMenuItem={toolTipOnPressMenuItem} actions={toolTipActions} />,
|
||||
() => <HeaderMenuButton onPressMenuItem={toolTipOnPressMenuItem} actions={toolTipActions} title={loc.cc.sort_by} />,
|
||||
[toolTipOnPressMenuItem, toolTipActions],
|
||||
);
|
||||
|
||||
|
@ -207,23 +207,19 @@ const About: React.FC = () => {
|
||||
<BlueTextCentered>
|
||||
w, h = {width}, {height}
|
||||
</BlueTextCentered>
|
||||
{process.env.NODE_ENV !== 'development' && (
|
||||
<>
|
||||
<BlueTextCentered>Unique ID: {getUniqueIdSync()}</BlueTextCentered>
|
||||
<View style={styles.copyToClipboard}>
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
onPress={() => {
|
||||
const stringToCopy = 'userId:' + getUniqueIdSync();
|
||||
A.logError('copied unique id');
|
||||
Clipboard.setString(stringToCopy);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.copyToClipboardText}>{loc.transactions.details_copy}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
<BlueTextCentered>Unique ID: {getUniqueIdSync()}</BlueTextCentered>
|
||||
<View style={styles.copyToClipboard}>
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
onPress={() => {
|
||||
const stringToCopy = 'userId:' + getUniqueIdSync();
|
||||
A.logError('copied unique id');
|
||||
Clipboard.setString(stringToCopy);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.copyToClipboardText}>{loc.transactions.details_copy}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<BlueSpacing20 />
|
||||
<BlueSpacing20 />
|
||||
</ScrollView>
|
||||
|
@ -44,6 +44,8 @@ import assert from 'assert';
|
||||
import useMenuElements from '../../hooks/useMenuElements';
|
||||
import { useSettings } from '../../hooks/context/useSettings';
|
||||
import { getClipboardContent } from '../../blue_modules/clipboard';
|
||||
import HandOffComponent from '../../components/HandOffComponent';
|
||||
import { HandOffActivityType } from '../../components/types';
|
||||
|
||||
const buttonFontSize =
|
||||
PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22
|
||||
@ -496,6 +498,13 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
||||
/>
|
||||
)}
|
||||
</FContainer>
|
||||
{wallet?.chain === Chain.ONCHAIN && wallet.type !== MultisigHDWallet.type && wallet.getXpub && wallet.getXpub() ? (
|
||||
<HandOffComponent
|
||||
title={wallet.getLabel()}
|
||||
type={HandOffActivityType.Xpub}
|
||||
url={`https://www.blockonomics.co/#/search?q=${wallet.getXpub()}`}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user