From 97bb4a693b89b6928844166f00c271ec9fddcef5 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Tue, 12 Nov 2024 21:44:00 -0400 Subject: [PATCH 01/30] OPS: Delete previous "APK available" comments --- fastlane/Fastfile | 46 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 1bbe43cbd..894e8da81 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -86,7 +86,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 @@ -116,30 +115,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 @@ -147,7 +176,6 @@ platform :android do UI.important("No PR number found. Skipping PR comment.") end end - end end From 7e75f579105f0068ae6f8e4b3535e3dbd4e436de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Wed, 13 Nov 2024 01:51:15 -0400 Subject: [PATCH 02/30] Update Fastfile --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 894e8da81..2c90ad551 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -177,7 +177,7 @@ platform :android do end end -end + # =========================== # iOS Lanes From 7dfd64ee4b48f23abf865fc31c15395fa1dd0dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Wed, 13 Nov 2024 01:56:15 -0400 Subject: [PATCH 03/30] Update Fastfile --- fastlane/Fastfile | 1 - 1 file changed, 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 2c90ad551..4f60859f6 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -585,5 +585,4 @@ lane :update_release_notes do |options| UI.error("No localization found for locale #{locale}") end end - end end From d785fe4ad0b35faee264853334edbdace372e33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Wed, 13 Nov 2024 02:08:30 -0400 Subject: [PATCH 04/30] Update Fastfile --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 4f60859f6..03a3cc943 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -176,7 +176,7 @@ platform :android do UI.important("No PR number found. Skipping PR comment.") end end - +end # =========================== From c925696df32690e0b30fd65a91a43fd781387063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Wed, 13 Nov 2024 02:20:43 -0400 Subject: [PATCH 05/30] Update Fastfile --- fastlane/Fastfile | 1 - 1 file changed, 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 03a3cc943..7c220ee4a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -585,4 +585,3 @@ lane :update_release_notes do |options| UI.error("No localization found for locale #{locale}") end end -end From c7849a9ec41c4c4d62df0de2d21fb3a22bffefe0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:14:25 +0000 Subject: [PATCH 06/30] Update dependency react-native-svg to v15.9.0 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f65ab1ba..995ecb70b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -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", diff --git a/package.json b/package.json index 1ac300473..5014955f5 100644 --- a/package.json +++ b/package.json @@ -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", From d221302b113dddfcf3a8ae330159f62f205b3741 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Wed, 13 Nov 2024 17:26:25 -0400 Subject: [PATCH 07/30] Update Fastfile --- fastlane/Fastfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index cb157a3f8..1f9292b41 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -440,7 +440,7 @@ lane :build_app_lane do end end end - +end # =========================== # Global Lanes # =========================== @@ -589,3 +589,5 @@ lane :update_release_notes do |options| UI.error("No localization found for locale #{locale}") end end +end +end \ No newline at end of file From 7485654960ae36adf5647edccc2bff7db50a70f7 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Thu, 14 Nov 2024 11:30:19 -0400 Subject: [PATCH 08/30] REF: useDeviceQUickactions --- components/CompanionDelegates.tsx | 12 ++--- components/Context/SettingsProvider.tsx | 5 +- components/DeviceQuickActions.windows.tsx | 21 -------- .../useDeviceQuickActions.ts | 54 +++++++++---------- hooks/useDeviceQuickActions.windows.ts | 17 ++++++ 5 files changed, 51 insertions(+), 58 deletions(-) delete mode 100644 components/DeviceQuickActions.windows.tsx rename components/DeviceQuickActions.tsx => hooks/useDeviceQuickActions.ts (77%) create mode 100644 hooks/useDeviceQuickActions.windows.ts diff --git a/components/CompanionDelegates.tsx b/components/CompanionDelegates.tsx index c219f81f6..d500796b1 100644 --- a/components/CompanionDelegates.tsx +++ b/components/CompanionDelegates.tsx @@ -28,8 +28,8 @@ import useMenuElements from '../hooks/useMenuElements'; import { useSettings } from '../hooks/context/useSettings'; import useWidgetCommunication from '../hooks/useWidgetCommunication'; import useWatchConnectivity from '../hooks/useWatchConnectivity'; +import useDeviceQuickActions from '../hooks/useDeviceQuickActions'; -const DeviceQuickActions = lazy(() => import('../components/DeviceQuickActions')); const HandOffComponentListener = lazy(() => import('../components/HandOffComponentListener')); const ClipboardContentType = Object.freeze({ @@ -40,12 +40,13 @@ const ClipboardContentType = Object.freeze({ const CompanionDelegates = () => { const { wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions, setSharedCosigner } = useStorage(); const appState = useRef(AppState.currentState); - const { isHandOffUseEnabled, isQuickActionsEnabled } = useSettings(); + const { isHandOffUseEnabled } = useSettings(); const clipboardContent = useRef(); useWatchConnectivity(); useWidgetCommunication(); useMenuElements(); + useDeviceQuickActions(); const processPushNotifications = useCallback(async () => { await new Promise(resolve => setTimeout(resolve, 200)); @@ -311,12 +312,7 @@ const CompanionDelegates = () => { }; }, [addListeners]); - return ( - - {isQuickActionsEnabled && } - {isHandOffUseEnabled && } - - ); + return {isHandOffUseEnabled && }; }; export default CompanionDelegates; diff --git a/components/Context/SettingsProvider.tsx b/components/Context/SettingsProvider.tsx index a00fd0a87..57203e9cc 100644 --- a/components/Context/SettingsProvider.tsx +++ b/components/Context/SettingsProvider.tsx @@ -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'; diff --git a/components/DeviceQuickActions.windows.tsx b/components/DeviceQuickActions.windows.tsx deleted file mode 100644 index 1c5057bac..000000000 --- a/components/DeviceQuickActions.windows.tsx +++ /dev/null @@ -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 => { - return false; -}; - -const DeviceQuickActions: React.FC & DeviceQuickActionsFunctions = () => { - return null; -}; - -DeviceQuickActions.popInitialAction = (): void => {}; - -export default DeviceQuickActions; diff --git a/components/DeviceQuickActions.tsx b/hooks/useDeviceQuickActions.ts similarity index 77% rename from components/DeviceQuickActions.tsx rename to hooks/useDeviceQuickActions.ts index 36d72d921..55a3e15d4 100644 --- a/components/DeviceQuickActions.tsx +++ b/hooks/useDeviceQuickActions.ts @@ -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 { } } -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 => { @@ -78,7 +77,7 @@ function DeviceQuickActions() { const popInitialAction = async (data: any): Promise => { 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; \ No newline at end of file diff --git a/hooks/useDeviceQuickActions.windows.ts b/hooks/useDeviceQuickActions.windows.ts new file mode 100644 index 000000000..f87b43d14 --- /dev/null +++ b/hooks/useDeviceQuickActions.windows.ts @@ -0,0 +1,17 @@ + +export const DeviceQuickActionsStorageKey = 'DeviceQuickActionsEnabled'; + +export const setEnabled = (): void => {}; + +export const getEnabled = async (): Promise => { + return false; +}; + +const useDeviceQuickActions = () => { + + const popInitialAction = (): void => {}; + return { popInitialAction }; +}; + + +export default useDeviceQuickActions; From ab98fe6616e34de74149a20abc154850524b792a Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Thu, 14 Nov 2024 14:15:08 -0400 Subject: [PATCH 09/30] REF: Sort By title --- components/HeaderMenuButton.tsx | 4 +++- loc/en.json | 9 +++++---- screen/send/CoinControl.tsx | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/components/HeaderMenuButton.tsx b/components/HeaderMenuButton.tsx index 84c4fe97b..6e3d8d1a4 100644 --- a/components/HeaderMenuButton.tsx +++ b/components/HeaderMenuButton.tsx @@ -9,9 +9,10 @@ interface HeaderMenuButtonProps { onPressMenuItem: (id: string) => void; actions?: Action[] | Action[][]; disabled?: boolean; + title?: string; } -const HeaderMenuButton: React.FC = ({ onPressMenuItem, actions, disabled }) => { +const HeaderMenuButton: React.FC = ({ onPressMenuItem, actions, disabled, title }) => { const { colors } = useTheme(); const styleProps = Platform.OS === 'android' ? { iconStyle: { transform: [{ rotate: '90deg' }] } } : {}; @@ -38,6 +39,7 @@ const HeaderMenuButton: React.FC = ({ onPressMenuItem, ac isMenuPrimaryAction onPressMenuItem={onPressMenuItem} actions={menuActions} + title={title} > diff --git a/loc/en.json b/loc/en.json index 37bde4290..e82dcd61a 100644 --- a/loc/en.json +++ b/loc/en.json @@ -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", diff --git a/screen/send/CoinControl.tsx b/screen/send/CoinControl.tsx index a49a403bc..3b015a9d1 100644 --- a/screen/send/CoinControl.tsx +++ b/screen/send/CoinControl.tsx @@ -485,7 +485,7 @@ const CoinControl: React.FC = () => { }, []); const HeaderRight = useMemo( - () => , + () => , [toolTipOnPressMenuItem, toolTipActions], ); From c2c499fb33a810699aa07607e59cf798716fbba5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:12:41 +0000 Subject: [PATCH 10/30] Update dependency detox to v20.28.0 --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f65ab1ba..930682199 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -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": { diff --git a/package.json b/package.json index 1ac300473..d20483b84 100644 --- a/package.json +++ b/package.json @@ -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", From 3d2bd0dd6ffa39bb974e3cda79fc9aa5f0da5130 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Thu, 14 Nov 2024 20:42:56 -0400 Subject: [PATCH 11/30] REF: useHandoff --- components/CompanionDelegates.tsx | 10 ++- components/HandOffComponent.ios.tsx | 33 +++++----- components/HandOffComponentListener.ios.tsx | 73 --------------------- components/HandOffComponentListener.tsx | 7 -- hooks/useHandoffListener.ios.ts | 60 +++++++++++++++++ hooks/useHandoffListener.ts | 3 + screen/wallets/WalletTransactions.tsx | 9 +++ 7 files changed, 92 insertions(+), 103 deletions(-) delete mode 100644 components/HandOffComponentListener.ios.tsx delete mode 100644 components/HandOffComponentListener.tsx create mode 100644 hooks/useHandoffListener.ios.ts create mode 100644 hooks/useHandoffListener.ts diff --git a/components/CompanionDelegates.tsx b/components/CompanionDelegates.tsx index d500796b1..d805604fd 100644 --- a/components/CompanionDelegates.tsx +++ b/components/CompanionDelegates.tsx @@ -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'; import useDeviceQuickActions from '../hooks/useDeviceQuickActions'; - -const HandOffComponentListener = lazy(() => import('../components/HandOffComponentListener')); +import useHandoffListener from '../hooks/useHandoffListener'; const ClipboardContentType = Object.freeze({ BITCOIN: 'BITCOIN', @@ -40,13 +38,13 @@ const ClipboardContentType = Object.freeze({ const CompanionDelegates = () => { const { wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions, setSharedCosigner } = useStorage(); const appState = useRef(AppState.currentState); - const { isHandOffUseEnabled } = useSettings(); const clipboardContent = useRef(); useWatchConnectivity(); useWidgetCommunication(); useMenuElements(); useDeviceQuickActions(); + useHandoffListener(); const processPushNotifications = useCallback(async () => { await new Promise(resolve => setTimeout(resolve, 200)); @@ -312,7 +310,7 @@ const CompanionDelegates = () => { }; }, [addListeners]); - return {isHandOffUseEnabled && }; + return null; }; export default CompanionDelegates; diff --git a/components/HandOffComponent.ios.tsx b/components/HandOffComponent.ios.tsx index f96a5241e..94200597c 100644 --- a/components/HandOffComponent.ios.tsx +++ b/components/HandOffComponent.ios.tsx @@ -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,31 @@ import { HandOffComponentProps } from './types'; const HandOffComponent: React.FC = props => { const { isHandOffUseEnabled } = useSettings(); - - if (process.env.NODE_ENV === 'development') { - console.debug('HandOffComponent: render'); - } - if (isHandOffUseEnabled) { - return ; - } - return null; + console.debug('HandOffComponent is rendering.'); + return isHandOffUseEnabled ? : 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); + } }; export const getIsHandOffUseEnabled = async (): Promise => { 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; } }; diff --git a/components/HandOffComponentListener.ios.tsx b/components/HandOffComponentListener.ios.tsx deleted file mode 100644 index 66a08b230..000000000 --- a/components/HandOffComponentListener.ios.tsx +++ /dev/null @@ -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; diff --git a/components/HandOffComponentListener.tsx b/components/HandOffComponentListener.tsx deleted file mode 100644 index 933fc9988..000000000 --- a/components/HandOffComponentListener.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -const HandOffComponentListener: React.FC = () => { - return null; -}; - -export default HandOffComponentListener; diff --git a/hooks/useHandoffListener.ios.ts b/hooks/useHandoffListener.ios.ts new file mode 100644 index 000000000..4e23454e7 --- /dev/null +++ b/hooks/useHandoffListener.ios.ts @@ -0,0 +1,60 @@ +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; +const eventEmitter = new NativeEventEmitter(EventEmitter); + +const useHandoffListener = () => { + const { walletsInitialized } = useStorage(); + const { isHandOffUseEnabled } = useSettings(); + const { navigate } = useExtendedNavigation(); + + const handleUserActivity = useCallback( + (data: UserActivityData) => { + const { activityType, userInfo } = data; + + 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}`); + } + }, + [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; \ No newline at end of file diff --git a/hooks/useHandoffListener.ts b/hooks/useHandoffListener.ts new file mode 100644 index 000000000..f0666c853 --- /dev/null +++ b/hooks/useHandoffListener.ts @@ -0,0 +1,3 @@ +const useHandoffListener = () => {}; + +export default useHandoffListener; diff --git a/screen/wallets/WalletTransactions.tsx b/screen/wallets/WalletTransactions.tsx index de4fb3f30..1ee221b5a 100644 --- a/screen/wallets/WalletTransactions.tsx +++ b/screen/wallets/WalletTransactions.tsx @@ -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 = ({ route }) => { /> )} + {wallet?.chain === Chain.ONCHAIN && wallet.type !== MultisigHDWallet.type && wallet.getXpub && wallet.getXpub() ? ( + + ) : null} ); }; From 52f52586b552481d5a900519bd246ff4772f6a27 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 21:33:10 +0000 Subject: [PATCH 12/30] Update dependency @react-native-async-storage/async-storage to v2.1.0 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f65ab1ba..0ed7ec8ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -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" diff --git a/package.json b/package.json index 1ac300473..e50ceb434 100644 --- a/package.json +++ b/package.json @@ -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", From b53af08c75832c1d6a4475b8ca7daa55a18a5083 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Fri, 15 Nov 2024 17:34:02 -0400 Subject: [PATCH 13/30] Update CoinControl.tsx --- screen/send/CoinControl.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/screen/send/CoinControl.tsx b/screen/send/CoinControl.tsx index 3b015a9d1..ccfa1e33c 100644 --- a/screen/send/CoinControl.tsx +++ b/screen/send/CoinControl.tsx @@ -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]); From b0dbe0966d76f70d9b2097f8af896b8864ca5728 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Fri, 15 Nov 2024 19:45:14 -0400 Subject: [PATCH 14/30] ADD: Manage Wallets Sort By --- components/TooltipMenu.tsx | 17 ++- components/icons/MoreOptionsButton.tsx | 50 ++++++++ components/icons/SettingsButton.tsx | 36 ++---- components/types.ts | 3 +- loc/en.json | 3 + screen/wallets/ManageWallets.tsx | 154 +++++++++++++++++++++++-- typings/CommonToolTipActions.ts | 22 ++++ 7 files changed, 247 insertions(+), 38 deletions(-) create mode 100644 components/icons/MoreOptionsButton.tsx diff --git a/components/TooltipMenu.tsx b/components/TooltipMenu.tsx index 39a5be982..28375c5af 100644 --- a/components/TooltipMenu.tsx +++ b/components/TooltipMenu.tsx @@ -54,7 +54,13 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref) => { 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 }, + attributes: { + disabled: subaction.disabled, + destructive: subaction.destructive, + hidden: subaction.hidden, + }, + subactions: subaction.subactions ? subaction.subactions.map(mapMenuItemForMenuView).filter(Boolean) : undefined, + displayInline: subaction.displayInline || false, })) || []; return { @@ -63,8 +69,12 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref) => { 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, destructive: action.destructive, hidden: action.hidden }, - subactions: subactions.length > 0 ? subactions : undefined, + attributes: { + disabled: action.disabled, + destructive: action.destructive, + hidden: action.hidden, + }, + subactions: subactions.length > 0 ? (subactions.filter(Boolean) as MenuAction[]) : undefined, displayInline: action.displayInline || false, }; }, []); @@ -86,6 +96,7 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref) => { .map(mapMenuItemForMenuView) .filter(item => item !== null) as MenuAction[], displayInline: true, + keepsMenuPresented: true, }; } else if (!Array.isArray(actionGroup) && actionGroup.id) { return mapMenuItemForMenuView(actionGroup); diff --git a/components/icons/MoreOptionsButton.tsx b/components/icons/MoreOptionsButton.tsx new file mode 100644 index 000000000..b4947edf4 --- /dev/null +++ b/components/icons/MoreOptionsButton.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { Icon } from '@rneui/themed'; +import { useTheme } from '../themes'; +import ToolTipMenu from '../TooltipMenu'; +import { Action } from '../types'; + +interface MoreOptionsButtonProps { + onPressMenuItem: (id: string) => void; + onPress?: () => void; + actions: Action[] | Action[][]; + testID?: string; + isMenuPrimaryAction: boolean; +} + +const MoreOptionsButton: React.FC = ({ + onPressMenuItem, + onPress, + actions, + testID = 'MoreOptionsButton', + isMenuPrimaryAction = false, +}) => { + const { colors } = useTheme(); + + return ( + + + + ); +}; + +export default MoreOptionsButton; + +const style = StyleSheet.create({ + buttonStyle: { + width: 30, + height: 30, + borderRadius: 15, + justifyContent: 'center', + alignContent: 'center', + }, +}); diff --git a/components/icons/SettingsButton.tsx b/components/icons/SettingsButton.tsx index 63ad9ab05..03f6666a1 100644 --- a/components/icons/SettingsButton.tsx +++ b/components/icons/SettingsButton.tsx @@ -1,14 +1,10 @@ import React, { useCallback, useMemo } from 'react'; -import { StyleSheet, TouchableOpacity } from 'react-native'; -import { Icon } from '@rneui/themed'; -import { useTheme } from '../themes'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; -import loc from '../../loc'; -import ToolTipMenu from '../TooltipMenu'; + import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; +import MoreOptionsButton from './MoreOptionsButton'; const SettingsButton = () => { - const { colors } = useTheme(); const { navigate } = useExtendedNavigation(); const onPress = () => { navigate('Settings'); @@ -29,28 +25,14 @@ const SettingsButton = () => { const actions = useMemo(() => [CommonToolTipActions.ManageWallet], []); return ( - - - - - + ); }; export default SettingsButton; - -const style = StyleSheet.create({ - buttonStyle: { - width: 30, - height: 30, - borderRadius: 15, - justifyContent: 'center', - alignContent: 'center', - }, -}); diff --git a/components/types.ts b/components/types.ts index cca1824ec..5da1f52da 100644 --- a/components/types.ts +++ b/components/types.ts @@ -11,11 +11,12 @@ export interface Action { menuState?: 'mixed' | boolean | undefined; displayInline?: boolean; // Indicates if subactions should be displayed inline or nested (iOS only) image?: string; + keepsMenuPresented?: boolean; imageColor?: ColorValue; destructive?: boolean; hidden?: boolean; disabled?: boolean; - subactions?: Action[]; // Nested/Inline actions (subactions) within an action + subactions?: Action[]; } export interface ToolTipMenuProps { diff --git a/loc/en.json b/loc/en.json index e82dcd61a..4d78945d9 100644 --- a/loc/en.json +++ b/loc/en.json @@ -383,6 +383,7 @@ "add_bitcoin_explain": "Simple and powerful Bitcoin wallet", "add_create": "Create", "total_balance": "Total Balance", + "balance": "Balance", "add_entropy_reset_title": "Reset Entropy", "add_entropy_reset_message": "Changing the wallet type will reset the current entropy. Do you want to proceed?", "add_entropy": "Entropy", @@ -473,6 +474,8 @@ "no_ln_wallet_error": "Before paying a Lightning invoice, you must first add a Lightning wallet.", "looks_like_bip38": "This looks like a password-protected private key (BIP38).", "manage_title": "Manage Wallets", + "sort_by_order": "Order", + "sort_by_property": "Property", "no_results_found": "No results found.", "please_continue_scanning": "Please continue scanning.", "select_no_bitcoin": "There are currently no Bitcoin wallets available.", diff --git a/screen/wallets/ManageWallets.tsx b/screen/wallets/ManageWallets.tsx index 7be138614..4a7d3ae3d 100644 --- a/screen/wallets/ManageWallets.tsx +++ b/screen/wallets/ManageWallets.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useLayoutEffect, useReducer, useCallback, useMemo, useRef } from 'react'; -import { StyleSheet, TouchableOpacity, Image, Text, Alert, I18nManager, Animated, LayoutAnimation } from 'react-native'; +import { StyleSheet, View, TouchableOpacity, Image, Text, Alert, I18nManager, Animated, LayoutAnimation } from 'react-native'; import { NestableScrollContainer, ScaleDecorator, @@ -25,12 +25,23 @@ import prompt from '../../helpers/prompt'; import HeaderRightButton from '../../components/HeaderRightButton'; import { ManageWalletsListItem } from '../../components/ManageWalletsListItem'; import { useSettings } from '../../hooks/context/useSettings'; +import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; +import MoreOptionsButton from '../../components/icons/MoreOptionsButton'; +import { Action } from '../../components/types'; enum ItemType { WalletSection = 'wallet', TransactionSection = 'transaction', } +enum SortOption { + Balance = 'balance', + Label = 'label', + MostRecent = 'mostRecent', + ASC = 'asc', + DESC = 'desc', +} + interface WalletItem { type: ItemType.WalletSection; data: TWallet; @@ -50,6 +61,7 @@ const SET_FILTERED_ORDER = 'SET_FILTERED_ORDER'; const SET_TEMP_ORDER = 'SET_TEMP_ORDER'; const REMOVE_WALLET = 'REMOVE_WALLET'; const SAVE_CHANGES = 'SAVE_CHANGES'; +const SET_CURRENT_SORT = 'SET_CURRENT_SORT'; interface SaveChangesAction { type: typeof SAVE_CHANGES; @@ -86,14 +98,20 @@ interface RemoveWalletAction { payload: string; // Wallet ID } -type Action = +interface SetCurrentSortAction { + type: typeof SET_CURRENT_SORT; + payload: SortOption; +} + +type ReducerAction = | SetSearchQueryAction | SetIsSearchFocusedAction | SetInitialOrderAction | SetFilteredOrderAction | SetTempOrderAction | SaveChangesAction - | RemoveWalletAction; + | RemoveWalletAction + | SetCurrentSortAction; interface State { searchQuery: string; @@ -102,6 +120,7 @@ interface State { tempOrder: Item[]; wallets: TWallet[]; txMetadata: TTXMetadata; + currentSort: SortOption; } const initialState: State = { @@ -111,13 +130,14 @@ const initialState: State = { tempOrder: [], wallets: [], txMetadata: {}, + currentSort: SortOption.Balance, }; const deepCopyWallets = (wallets: TWallet[]): TWallet[] => { return wallets.map(wallet => Object.assign(Object.create(Object.getPrototypeOf(wallet)), wallet)); }; -const reducer = (state: State, action: Action): State => { +const reducer = (state: State, action: ReducerAction): State => { switch (action.type) { case SET_SEARCH_QUERY: return { ...state, searchQuery: action.payload }; @@ -181,8 +201,10 @@ const reducer = (state: State, action: Action): State => { tempOrder: updatedOrder, }; } + case SET_CURRENT_SORT: + return { ...state, currentSort: action.payload }; default: - throw new Error(`Unhandled action type: ${(action as Action).type}`); + throw new Error(`Unhandled action type: ${(action as ReducerAction).type}`); } }; @@ -263,11 +285,126 @@ const ManageWallets: React.FC = () => { [goBack, closeImage], ); + const moreOptionsActions = useMemo((): Action[] | Action[][] => { + return [ + { + id: 'sort_by_menu', + text: loc.cc.sort_by, + subactions: [ + { + id: 'sort_by_order', + displayInline: true, + text: loc.wallets.sort_by_order, + keepsMenuPresented: true, + subactions: [ + { ...CommonToolTipActions.SortASC, menuState: state.currentSort === SortOption.ASC }, + { ...CommonToolTipActions.SortDESC, menuState: state.currentSort === SortOption.DESC }, + ], + }, + { + id: 'sort_by_wallet', + displayInline: true, + text: loc.wallets.sort_by_property, + subactions: [ + { + ...CommonToolTipActions.SortBalance, + menuState: state.currentSort === SortOption.Balance, + disabled: state.currentSort === SortOption.Balance, + }, + { + ...CommonToolTipActions.SortLabel, + menuState: state.currentSort === SortOption.Label, + disabled: state.currentSort === SortOption.Label, + }, + { + ...CommonToolTipActions.MostRecentTransaction, + menuState: state.currentSort === SortOption.MostRecent, + disabled: state.currentSort === SortOption.MostRecent, + }, + ], + }, + { ...CommonToolTipActions.Reset }, + ], + }, + ]; + }, [state.currentSort]); + + const moreOptionsOnPressMenuItem = useCallback( + (id: string) => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + switch (id) { + case CommonToolTipActions.SortASC.id: { + dispatch({ type: SET_CURRENT_SORT, payload: SortOption.ASC }); + const sortedWallets = state.tempOrder + .filter((item): item is WalletItem => item.type === ItemType.WalletSection) + .sort((a, b) => a.data.getLabel()!.localeCompare(b.data.getLabel()!)); + dispatch({ type: SET_TEMP_ORDER, payload: sortedWallets }); + break; + } + case CommonToolTipActions.SortDESC.id: { + dispatch({ type: SET_CURRENT_SORT, payload: SortOption.DESC }); + const sortedWallets = state.tempOrder + .filter((item): item is WalletItem => item.type === ItemType.WalletSection) + .sort((a, b) => b.data.getLabel()!.localeCompare(a.data.getLabel()!)); + dispatch({ type: SET_TEMP_ORDER, payload: sortedWallets }); + break; + } + case CommonToolTipActions.SortBalance.id: { + dispatch({ type: SET_CURRENT_SORT, payload: SortOption.Balance }); + const sortedWallets = state.tempOrder + .filter((item): item is WalletItem => item.type === ItemType.WalletSection) + .sort((a, b) => a.data.getBalance() - b.data.getBalance()); + dispatch({ type: SET_TEMP_ORDER, payload: sortedWallets }); + break; + } + case CommonToolTipActions.SortLabel.id: { + dispatch({ type: SET_CURRENT_SORT, payload: SortOption.Label }); + const sortedWalletsByLabel = state.tempOrder + .filter((item): item is WalletItem => item.type === ItemType.WalletSection) + .sort((a, b) => a.data.getLabel()!.localeCompare(b.data.getLabel()!)); + dispatch({ type: SET_TEMP_ORDER, payload: sortedWalletsByLabel }); + break; + } + case CommonToolTipActions.MostRecentTransaction.id: { + dispatch({ type: SET_CURRENT_SORT, payload: SortOption.MostRecent }); + const sortedWalletsByMostRecent = state.tempOrder + .filter((item): item is WalletItem => item.type === ItemType.WalletSection) + .sort((a, b) => { + return b.data.getTransactions()[0]?.time - a.data.getTransactions()[0]?.time; + }); + dispatch({ type: SET_TEMP_ORDER, payload: sortedWalletsByMostRecent }); + break; + } + case CommonToolTipActions.Reset.id: { + dispatch({ type: SET_TEMP_ORDER, payload: state.order }); + break; + } + } + }, + [state.order, state.tempOrder], + ); + const SaveButton = useMemo( () => , [handleClose, hasUnsavedChanges], ); + const MoreButton = useMemo( + () => , + [moreOptionsActions, moreOptionsOnPressMenuItem], + ); + + const HeaderRight = useCallback( + () => ( + <> + {MoreButton} + + {SaveButton} + + ), + [MoreButton, SaveButton], + ); + useLayoutEffect(() => { const searchBarOptions = { hideWhenScrolling: false, @@ -280,10 +417,10 @@ const ManageWallets: React.FC = () => { setOptions({ headerLeft: () => HeaderLeftButton, - headerRight: () => SaveButton, + headerRight: HeaderRight, headerSearchBarOptions: searchBarOptions, }); - }, [setOptions, HeaderLeftButton, SaveButton]); + }, [setOptions, HeaderLeftButton, SaveButton, MoreButton, HeaderRight]); useFocusEffect( useCallback(() => { @@ -577,4 +714,7 @@ const styles = StyleSheet.create({ dimmedText: { opacity: 0.8, }, + separation: { + width: 16, + }, }); diff --git a/typings/CommonToolTipActions.ts b/typings/CommonToolTipActions.ts index d7b9d3e37..cd2010095 100644 --- a/typings/CommonToolTipActions.ts +++ b/typings/CommonToolTipActions.ts @@ -50,6 +50,9 @@ const keys = { SortValue: 'sortValue', SortLabel: 'sortLabel', SortStatus: 'sortStatus', + SortBalance: 'sortBalance', + MostRecentTransaction: 'mostRecentTransaction', + Reset: 'reset', } as const; const icons = { @@ -94,6 +97,8 @@ const icons = { ClearClipboard: { iconValue: 'clipboard' }, SortASC: { iconValue: 'arrow.down.to.line' }, SortDESC: { iconValue: 'arrow.up.to.line' }, + MostRecentTransaction: { iconValue: 'clock' }, + Reset: { iconValue: 'arrow.counterclockwise' }, } as const; export const CommonToolTipActions = { @@ -309,6 +314,11 @@ export const CommonToolTipActions = { id: keys.ResetToDefault, text: loc.settings.electrum_reset, }, + Reset: { + id: keys.Reset, + text: loc.receive.reset, + Icon: icons.Reset, + }, ClearHistory: { id: keys.ClearHistory, text: loc.settings.electrum_clear, @@ -329,11 +339,13 @@ export const CommonToolTipActions = { id: keys.SortASC, text: loc.cc.sort_asc, icon: icons.SortASC, + keepsMenuPresented: true, }, SortDESC: { id: keys.SortDESC, text: loc.cc.sort_desc, icon: icons.SortDESC, + keepsMenuPresented: true, }, SortHeight: { id: keys.SortHeight, @@ -351,4 +363,14 @@ export const CommonToolTipActions = { id: keys.SortStatus, text: loc.cc.sort_status, }, + SortBalance: { + id: keys.SortBalance, + text: loc.wallets.balance, + icon: icons.ViewInBitcoin, + }, + MostRecentTransaction: { + id: keys.MostRecentTransaction, + text: loc.transactions.details_title, + icon: icons.MostRecentTransaction, + }, } as const; From 87113d99d28de30ba79bd6770d19c456e0904adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Fri, 15 Nov 2024 19:52:43 -0400 Subject: [PATCH 15/30] Update components/HandOffComponent.ios.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- components/HandOffComponent.ios.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/HandOffComponent.ios.tsx b/components/HandOffComponent.ios.tsx index 94200597c..0a262f3fe 100644 --- a/components/HandOffComponent.ios.tsx +++ b/components/HandOffComponent.ios.tsx @@ -22,6 +22,7 @@ export const setIsHandOffUseEnabled = async (value: boolean) => { console.debug('setIsHandOffUseEnabled', value); } catch (error) { console.error('Error setting handoff enabled status:', error); + throw error; // Propagate error to caller } }; From d27e3bddeec6febe7f636da2786af23b2897af15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Fri, 15 Nov 2024 19:53:03 -0400 Subject: [PATCH 16/30] Update hooks/useHandoffListener.ios.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- hooks/useHandoffListener.ios.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hooks/useHandoffListener.ios.ts b/hooks/useHandoffListener.ios.ts index 4e23454e7..2762b84af 100644 --- a/hooks/useHandoffListener.ios.ts +++ b/hooks/useHandoffListener.ios.ts @@ -13,8 +13,8 @@ interface UserActivityData { }; } -const { EventEmitter } = NativeModules; -const eventEmitter = new NativeEventEmitter(EventEmitter); +const EventEmitter = NativeModules.EventEmitter; +const eventEmitter = EventEmitter ? new NativeEventEmitter(EventEmitter) : null; const useHandoffListener = () => { const { walletsInitialized } = useStorage(); From e91a24da853532a99715af47a5247a2ff4ab3fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Fri, 15 Nov 2024 19:53:44 -0400 Subject: [PATCH 17/30] Update screen/wallets/WalletTransactions.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- screen/wallets/WalletTransactions.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/screen/wallets/WalletTransactions.tsx b/screen/wallets/WalletTransactions.tsx index 1ee221b5a..249a6cbbc 100644 --- a/screen/wallets/WalletTransactions.tsx +++ b/screen/wallets/WalletTransactions.tsx @@ -499,10 +499,9 @@ const WalletTransactions: React.FC = ({ route }) => { )} {wallet?.chain === Chain.ONCHAIN && wallet.type !== MultisigHDWallet.type && wallet.getXpub && wallet.getXpub() ? ( - navigation.navigate('WalletXpub', { walletID: wallet.getID(), xpub: wallet.getXpub() })} + title={loc.wallets.view_xpub} /> ) : null} From 84242c03ec9dff39acd1125699c64e2f51a33d85 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Fri, 15 Nov 2024 19:58:11 -0400 Subject: [PATCH 18/30] wip --- hooks/useHandoffListener.ios.ts | 35 +++++++++++++++------------ screen/wallets/WalletTransactions.tsx | 7 +++--- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/hooks/useHandoffListener.ios.ts b/hooks/useHandoffListener.ios.ts index 2762b84af..df9ecec3d 100644 --- a/hooks/useHandoffListener.ios.ts +++ b/hooks/useHandoffListener.ios.ts @@ -24,19 +24,22 @@ const useHandoffListener = () => { const handleUserActivity = useCallback( (data: UserActivityData) => { const { activityType, userInfo } = data; - - 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}`); + 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], @@ -45,16 +48,16 @@ const useHandoffListener = () => { useEffect(() => { if (!walletsInitialized || !isHandOffUseEnabled) return; - const activitySubscription = eventEmitter.addListener('onUserActivityOpen', handleUserActivity); + const activitySubscription = eventEmitter?.addListener('onUserActivityOpen', handleUserActivity); EventEmitter.getMostRecentUserActivity?.() .then(handleUserActivity) .catch(() => console.debug('No userActivity object sent')); return () => { - activitySubscription.remove(); + activitySubscription?.remove(); }; }, [walletsInitialized, isHandOffUseEnabled, handleUserActivity]); }; -export default useHandoffListener; \ No newline at end of file +export default useHandoffListener; diff --git a/screen/wallets/WalletTransactions.tsx b/screen/wallets/WalletTransactions.tsx index 249a6cbbc..1ee221b5a 100644 --- a/screen/wallets/WalletTransactions.tsx +++ b/screen/wallets/WalletTransactions.tsx @@ -499,9 +499,10 @@ const WalletTransactions: React.FC = ({ route }) => { )} {wallet?.chain === Chain.ONCHAIN && wallet.type !== MultisigHDWallet.type && wallet.getXpub && wallet.getXpub() ? ( -