From 9b756c5fdec025c22c0b9a707f236a58769a3675 Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Tue, 4 Mar 2025 11:36:01 +0100 Subject: [PATCH] feat(ui): add drawer menu --- e2e/backup.e2e.js | 3 +- e2e/boost.e2e.js | 3 +- e2e/helpers.js | 6 +- e2e/lightning.e2e.js | 9 +- e2e/lnurl.e2e.js | 3 +- e2e/numberpad.e2e.js | 3 +- e2e/onchain.e2e.js | 3 +- e2e/security.e2e.js | 12 +- e2e/send.e2e.js | 9 +- e2e/settings.e2e.js | 67 +- e2e/slashtags.e2e.js | 12 +- e2e/transfer.e2e.js | 18 +- index.js | 6 +- package.json | 1 + src/App.tsx | 2 +- src/AppOnboarded.tsx | 127 +-- src/Root.tsx | 17 +- src/assets/icons/header.ts | 5 - src/assets/icons/settings.ts | 291 ++++--- src/assets/icons/wallet.ts | 819 ++++++++++-------- src/components/AppStatus.tsx | 135 +++ src/components/TabBar.tsx | 12 +- src/components/Widgets.tsx | 6 +- src/hooks/useAppStateHandler.ts | 41 + src/hooks/useAppStatus.ts | 27 + src/hooks/useNetworkConnectivity.ts | 42 + src/hooks/useWalletStartup.ts | 34 + .../{onboarding => }/OnboardingNavigator.tsx | 24 +- .../{settings => }/SettingsNavigator.tsx | 94 +- .../{transfer => }/TransferNavigator.tsx | 48 +- .../{wallet => }/WalletNavigator.tsx | 19 +- .../bottom-sheet/BackupNavigation.tsx | 6 +- .../BottomSheetNavigationContainer.tsx | 24 + .../bottom-sheet/LNURLWithdrawNavigation.tsx | 6 +- .../bottom-sheet/OrangeTicketNavigation.tsx | 13 +- src/navigation/bottom-sheet/PINNavigation.tsx | 8 +- .../bottom-sheet/ProfileLinkNavigation.tsx | 6 +- .../bottom-sheet/ReceiveNavigation.tsx | 6 +- .../bottom-sheet/SendNavigation.tsx | 8 +- .../bottom-sheet/TreasureHuntNavigation.tsx | 6 +- src/navigation/root/DrawerContent.tsx | 147 ++++ src/navigation/root/DrawerNavigator.tsx | 42 + .../root/RootNavigationContainer.tsx | 77 ++ src/navigation/root/RootNavigator.tsx | 73 +- src/navigation/types/index.ts | 8 +- src/screens/Activity/ActivityDetail.tsx | 4 +- src/screens/Activity/ActivityFiltered.tsx | 2 + src/screens/Recovery/RecoveryNavigator.tsx | 4 +- src/screens/Settings/AppStatus/index.tsx | 267 +++--- src/screens/Settings/Backup/ExportToPhone.tsx | 136 --- src/screens/Settings/Backup/Metadata.tsx | 2 +- src/screens/Settings/BackupSettings/index.tsx | 6 +- src/screens/Wallets/BoostPrompt.tsx | 2 +- src/screens/Wallets/Header.tsx | 78 +- src/screens/Wallets/LNURLPay/Confirm.tsx | 4 +- src/screens/Wallets/NewTxPrompt.tsx | 2 +- .../Wallets/Receive/ReceiveGeoBlocked.tsx | 2 +- src/screens/Wallets/Send/Pending.tsx | 2 +- src/screens/Wallets/Send/ReviewAndSend.tsx | 4 +- src/screens/Wallets/Send/Success.tsx | 2 +- src/store/reselect/ui.ts | 228 +++-- src/store/shapes/backup.ts | 3 +- src/store/slices/backup.ts | 2 +- src/store/types/backup.ts | 11 +- src/store/types/ui.ts | 2 + src/store/utils/backup.ts | 12 +- src/styles/components.ts | 22 +- src/styles/icons.ts | 203 ++--- src/styles/text.ts | 7 + src/styles/themes.ts | 6 - src/utils/backup/backups-subscriber.tsx | 3 +- src/utils/i18n/locales/ca/settings.json | 2 +- src/utils/i18n/locales/cs/settings.json | 4 +- src/utils/i18n/locales/de/settings.json | 4 +- src/utils/i18n/locales/en/settings.json | 4 +- src/utils/i18n/locales/en/wallet.json | 23 + src/utils/i18n/locales/es_419/settings.json | 2 +- src/utils/i18n/locales/es_ES/settings.json | 4 +- src/utils/i18n/locales/fr/settings.json | 4 +- src/utils/i18n/locales/it/settings.json | 4 +- src/utils/i18n/locales/nl/settings.json | 4 +- src/utils/i18n/locales/pt_BR/settings.json | 4 +- src/utils/i18n/locales/ru/settings.json | 4 +- src/utils/scanner/scanner.ts | 2 +- src/utils/slashtags/index.ts | 2 +- src/utils/startup/index.ts | 2 +- src/utils/wallet/transactions.ts | 2 +- yarn.lock | 35 + 88 files changed, 1961 insertions(+), 1489 deletions(-) delete mode 100644 src/assets/icons/header.ts create mode 100644 src/components/AppStatus.tsx create mode 100644 src/hooks/useAppStateHandler.ts create mode 100644 src/hooks/useAppStatus.ts create mode 100644 src/hooks/useNetworkConnectivity.ts create mode 100644 src/hooks/useWalletStartup.ts rename src/navigation/{onboarding => }/OnboardingNavigator.tsx (73%) rename src/navigation/{settings => }/SettingsNavigator.tsx (60%) rename src/navigation/{transfer => }/TransferNavigator.tsx (66%) rename src/navigation/{wallet => }/WalletNavigator.tsx (66%) create mode 100644 src/navigation/bottom-sheet/BottomSheetNavigationContainer.tsx create mode 100644 src/navigation/root/DrawerContent.tsx create mode 100644 src/navigation/root/DrawerNavigator.tsx create mode 100644 src/navigation/root/RootNavigationContainer.tsx delete mode 100644 src/screens/Settings/Backup/ExportToPhone.tsx diff --git a/e2e/backup.e2e.js b/e2e/backup.e2e.js index 7c8645e53..e9af07f59 100644 --- a/e2e/backup.e2e.js +++ b/e2e/backup.e2e.js @@ -84,7 +84,8 @@ d('Backup', () => { // change currency to GBP await element(by.id('TotalBalance')).tap(); // switch to local currency - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('GeneralSettings')).tap(); await element(by.id('CurrenciesSettings')).tap(); await element(by.text('GBP (£)')).tap(); diff --git a/e2e/boost.e2e.js b/e2e/boost.e2e.js index 1a23f1fd4..0b3374577 100644 --- a/e2e/boost.e2e.js +++ b/e2e/boost.e2e.js @@ -51,7 +51,8 @@ d('Boost', () => { } // switch off RBF mode - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); if (!__DEV__) { await element(by.id('DevOptions')).multiTap(5); // enable dev mode } diff --git a/e2e/helpers.js b/e2e/helpers.js index 498cf1681..dd0db1221 100644 --- a/e2e/helpers.js +++ b/e2e/helpers.js @@ -185,7 +185,8 @@ export const waitForActiveChannel = async (lnd, nodeId, maxRetries = 20) => { }; export const getSeed = async () => { - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('BackupSettings')).tap(); await element(by.id('BackupWallet')).tap(); // animation @@ -247,7 +248,8 @@ export const restoreWallet = async (seed, passphrase) => { }; export const waitForBackup = async () => { - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('BackupSettings')).tap(); await waitFor(element(by.id('AllSynced'))) .toBeVisible() diff --git a/e2e/lightning.e2e.js b/e2e/lightning.e2e.js index bb0c344f5..6bc9b5176 100644 --- a/e2e/lightning.e2e.js +++ b/e2e/lightning.e2e.js @@ -71,7 +71,8 @@ d('Lightning', () => { const { identityPubkey: lndNodeID } = await lnd.getInfo(); // get LDK Node id - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).tap(); // wait for LDK to start await sleep(5000); @@ -112,7 +113,8 @@ d('Lightning', () => { await waitForActiveChannel(lnd, ldkNodeId); // check channel status - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).atIndex(0).tap(); await element(by.id('Channels')).tap(); await element(by.id('Channel')).atIndex(0).tap(); @@ -331,7 +333,8 @@ d('Lightning', () => { await element(by.id('NavigationClose')).tap(); // check channel status - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).tap(); await sleep(100); await element(by.id('Channels')).tap(); diff --git a/e2e/lnurl.e2e.js b/e2e/lnurl.e2e.js index 4b6e9c477..699f19cc4 100644 --- a/e2e/lnurl.e2e.js +++ b/e2e/lnurl.e2e.js @@ -90,7 +90,8 @@ d('LNURL', () => { } // get LDK Node id - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); if (!__DEV__) { await element(by.id('DevOptions')).multiTap(5); // enable dev mode } diff --git a/e2e/numberpad.e2e.js b/e2e/numberpad.e2e.js index 9275d002b..5a56572e3 100644 --- a/e2e/numberpad.e2e.js +++ b/e2e/numberpad.e2e.js @@ -85,7 +85,8 @@ d('NumberPad', () => { } // switch to classic denomination - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('GeneralSettings')).tap(); await element(by.id('UnitSettings')).tap(); await element(by.id('DenominationClassic')).tap(); diff --git a/e2e/onchain.e2e.js b/e2e/onchain.e2e.js index 0a32d7931..d739b868f 100644 --- a/e2e/onchain.e2e.js +++ b/e2e/onchain.e2e.js @@ -246,7 +246,8 @@ d('Onchain', () => { const coreAddress = await rpc.getNewAddress(); // enable warning for sending over 100$ to test multiple warning dialogs - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('SecuritySettings')).tap(); await element(by.id('SendAmountWarning')).tap(); await element(by.id('NavigationClose')).atIndex(0).tap(); diff --git a/e2e/security.e2e.js b/e2e/security.e2e.js index 682997ae9..a96830b09 100644 --- a/e2e/security.e2e.js +++ b/e2e/security.e2e.js @@ -71,7 +71,8 @@ d('Settings Security And Privacy', () => { await device.setBiometricEnrollment(true); - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('SecuritySettings')).tap(); await element(by.id('PINCode')).tap(); await element(by.id('SecureWallet-button-continue')).tap(); @@ -164,7 +165,8 @@ d('Settings Security And Privacy', () => { await element(by.id('Close')).tap(); // test PIN on idle and disable it after - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('SecuritySettings')).tap(); // FIXME: this fails too often @@ -223,7 +225,8 @@ d('Settings Security And Privacy', () => { await element(by.id('Close')).tap(); // disable PIN, restart the app, it should not ask for it - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('SecuritySettings')).tap(); await element(by.id('PINCode')).tap(); await element(by.id('DisablePin')).tap(); @@ -235,7 +238,8 @@ d('Settings Security And Privacy', () => { .withTimeout(10000); // enable PIN for last test - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('SecuritySettings')).tap(); await element(by.id('PINCode')).tap(); await element(by.id('SecureWallet-button-continue')).tap(); diff --git a/e2e/send.e2e.js b/e2e/send.e2e.js index f11ca8a9e..f7e6d54ef 100644 --- a/e2e/send.e2e.js +++ b/e2e/send.e2e.js @@ -185,7 +185,8 @@ d('Send', () => { const { identityPubkey: lndNodeID } = await lnd.getInfo(); // get LDK Node id - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).tap(); // wait for LDK to start await sleep(5000); @@ -226,7 +227,8 @@ d('Send', () => { await waitForActiveChannel(lnd, ldkNodeId); // check channel status - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).atIndex(0).tap(); await element(by.id('Channels')).tap(); await element(by.id('Channel')).atIndex(0).tap(); @@ -455,7 +457,8 @@ d('Send', () => { const { paymentRequest: invoice7 } = await lnd.addInvoice({ value: 1000 }); // enable quickpay - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('GeneralSettings')).tap(); await element(by.id('QuickpaySettings')).tap(); await element(by.id('QuickpayIntro-button')).tap(); diff --git a/e2e/settings.e2e.js b/e2e/settings.e2e.js index 1fef292ef..d99623c9e 100644 --- a/e2e/settings.e2e.js +++ b/e2e/settings.e2e.js @@ -58,7 +58,8 @@ d('Settings', () => { ).toHaveText('$'); // switch to GBP - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('GeneralSettings')).tap(); await element(by.id('CurrenciesSettings')).tap(); await element(by.text('GBP (£)')).tap(); @@ -72,7 +73,8 @@ d('Settings', () => { await element(by.id('TotalBalance')).tap(); // switch to USD - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('GeneralSettings')).tap(); await element(by.id('CurrenciesSettings')).tap(); await element(by.text('USD ($)')).tap(); @@ -95,7 +97,8 @@ d('Settings', () => { by.id('Value').withAncestor(by.id('UnitSettings')), ); - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('GeneralSettings')).tap(); // check default unit await expect(unitRow).toHaveText('Bitcoin'); @@ -110,7 +113,8 @@ d('Settings', () => { await expect(balance).toHaveText('0.00'); // switch back to BTC - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('GeneralSettings')).tap(); await element(by.id('UnitSettings')).tap(); await element(by.id('Bitcoin')).tap(); @@ -120,7 +124,8 @@ d('Settings', () => { await expect(balance).toHaveText('0'); // switch to classic denomination - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('GeneralSettings')).tap(); await element(by.id('UnitSettings')).tap(); await element(by.id('DenominationClassic')).tap(); @@ -137,7 +142,8 @@ d('Settings', () => { return; } - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('GeneralSettings')).tap(); // switch to Fast @@ -172,7 +178,8 @@ d('Settings', () => { } // no tags, menu entry should be hidden - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('GeneralSettings')).tap(); await expect(element(by.id('TagsSettings'))).not.toBeVisible(); await element(by.id('NavigationClose')).atIndex(0).tap(); @@ -191,7 +198,8 @@ d('Settings', () => { await sleep(1000); // open tag manager, delete tag - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('GeneralSettings')).tap(); await element(by.id('TagsSettings')).tap(); await expect(element(by.text(tag))).toBeVisible(); @@ -214,7 +222,8 @@ d('Settings', () => { return; } - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('About')).tap(); await expect(element(by.id('AboutLogo'))).toBeVisible(); @@ -242,7 +251,8 @@ d('Settings', () => { await expect(element(by.id('ShowBalance'))).toBeVisible(); // Disable 'swipe to hide balance' - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('SecuritySettings')).tap(); await element(by.id('SwipeBalanceToHide')).tap(); await element(by.id('NavigationClose')).atIndex(0).tap(); @@ -255,7 +265,8 @@ d('Settings', () => { await expect(element(by.id('ShowBalance'))).not.toBeVisible(); // Enable 'hide balance on open' - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('SecuritySettings')).tap(); await element(by.id('SwipeBalanceToHide')).tap(); await element(by.id('HideBalanceOnOpen')).tap(); @@ -275,7 +286,8 @@ d('Settings', () => { return; } - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('BackupSettings')).tap(); await element(by.id('ResetAndRestore')).tap(); // just check if this screen can be opened await element(by.id('NavigationBack')).atIndex(0).tap(); @@ -327,7 +339,8 @@ d('Settings', () => { await sleep(1000); // check same address in Address Viewer - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).tap(); await element(by.id('WebRelay')).swipe('up'); await element(by.id('AddressViewer')).tap(); @@ -389,7 +402,8 @@ d('Settings', () => { await sleep(1000); // switch back to Native segwit - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).tap(); await element(by.id('AddressTypePreference')).tap(); await element(by.id('p2wpkh')).tap(); @@ -403,7 +417,8 @@ d('Settings', () => { return; } - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); if (!__DEV__) { await element(by.id('DevOptions')).multiTap(5); // enable dev mode } @@ -438,7 +453,8 @@ d('Settings', () => { return; } - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).tap(); await element(by.id('ElectrumConfig')).tap(); @@ -527,7 +543,8 @@ d('Settings', () => { return; } - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).tap(); await element(by.id('WebRelay')).tap(); @@ -561,7 +578,8 @@ d('Settings', () => { return; } - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).tap(); await element(by.id('RGSServer')).tap(); @@ -610,7 +628,8 @@ d('Settings', () => { await expect(element(by.id('Suggestion-lightning'))).not.toBeVisible(); // reset suggestions - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).tap(); await element(by.id('WebRelay')).swipe('up'); await element(by.id('ResetSuggestions')).tap(); @@ -628,7 +647,8 @@ d('Settings', () => { return; } - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); if (!__DEV__) { await element(by.id('DevOptions')).multiTap(5); // enable dev mode } @@ -653,15 +673,16 @@ d('Settings', () => { return; } - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('Support')).tap(); await element(by.id('AppStatus')).tap(); await expect(element(by.id('Status-internet'))).toBeVisible(); - await expect(element(by.id('Status-bitcoin_node'))).toBeVisible(); + await expect(element(by.id('Status-electrum'))).toBeVisible(); await expect(element(by.id('Status-lightning_node'))).toBeVisible(); await expect(element(by.id('Status-lightning_connection'))).toBeVisible(); - await expect(element(by.id('Status-full_backup'))).toBeVisible(); + await expect(element(by.id('Status-backup'))).toBeVisible(); await element(by.id('NavigationClose')).atIndex(0).tap(); diff --git a/e2e/slashtags.e2e.js b/e2e/slashtags.e2e.js index da57e755b..5b9a739dd 100644 --- a/e2e/slashtags.e2e.js +++ b/e2e/slashtags.e2e.js @@ -119,7 +119,8 @@ d('Profile and Contacts', () => { await element(by.id('NavigationClose')).tap(); // ADD CONTACTS - await element(by.id('HeaderContactsButton')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerContacts')).tap(); await element(by.id('ContactsOnboarding-button')).tap(); // self @@ -162,7 +163,8 @@ d('Profile and Contacts', () => { await element(by.id('NavigationClose')).tap(); // FILTER CONTACTS - await element(by.id('HeaderContactsButton')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerContacts')).tap(); await expect(element(by.text(satoshi.name))).toBeVisible(); await expect(element(by.text(hal.name2))).toBeVisible(); await element(by.id('ContactsSearchInput')).typeText('Satoshi\n'); @@ -180,7 +182,8 @@ d('Profile and Contacts', () => { .toBeVisible() .withTimeout(60000); - await element(by.id('HeaderContactsButton')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerContacts')).tap(); // check un-edited contact await expect(element(by.text(satoshi.name))).toBeVisible(); // check edited contact retains new name @@ -231,7 +234,8 @@ d('Profile and Contacts', () => { .toBeVisible() .withTimeout(60000); - await element(by.id('HeaderContactsButton')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerContacts')).tap(); await expect(element(by.text(satoshi.name))).toBeVisible(); await expect(element(by.text(hal.name1))).not.toBeVisible(); await expect(element(by.text(hal.name2))).not.toBeVisible(); diff --git a/e2e/transfer.e2e.js b/e2e/transfer.e2e.js index f034e2aec..df6f53706 100644 --- a/e2e/transfer.e2e.js +++ b/e2e/transfer.e2e.js @@ -81,7 +81,8 @@ d('Transfer', () => { await element(by.id('NewTxPrompt')).swipe('down'); // close Receive screen // switch to USD - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('GeneralSettings')).tap(); await element(by.id('CurrenciesSettings')).tap(); await element(by.text('EUR (€)')).tap(); @@ -196,7 +197,8 @@ d('Transfer', () => { // check channel status await element(by.id('NavigationClose')).tap(); await sleep(1000); - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).atIndex(0).tap(); await element(by.id('Channels')).tap(); await element(by.id('Channel')).atIndex(0).tap(); @@ -265,7 +267,8 @@ d('Transfer', () => { await element(by.id('NewTxPrompt')).swipe('down'); // Get LDK node id - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).tap(); // wait for LDK to start await sleep(5000); @@ -281,7 +284,8 @@ d('Transfer', () => { const { identityPubkey: lndNodeId } = await lnd.getInfo(); // Connect to LND - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).tap(); await element(by.id('Channels')).tap(); await element(by.id('NavigationAction')).tap(); @@ -348,7 +352,8 @@ d('Transfer', () => { ).not.toBeVisible(); // check channel status - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).atIndex(0).tap(); await element(by.id('Channels')).tap(); await element(by.id('Channel')).atIndex(0).tap(); @@ -397,7 +402,8 @@ d('Transfer', () => { await element(by.id('TransferSuccess-button')).tap(); // check channel is closed - await element(by.id('Settings')).tap(); + await element(by.id('HeaderMenu')).tap(); + await element(by.id('DrawerSettings')).tap(); await element(by.id('AdvancedSettings')).atIndex(0).tap(); await element(by.id('Channels')).tap(); await expect(element(by.text('Connection 1'))).not.toBeVisible(); diff --git a/index.js b/index.js index 6327cc15b..2d6c0da74 100644 --- a/index.js +++ b/index.js @@ -1,12 +1,10 @@ // NOTE: import order matters - +import 'react-native-gesture-handler' // must be first import './shim'; import './src/utils/fetch'; import './src/utils/ignoreLogs'; import { AppRegistry, Text, TextInput } from 'react-native'; -import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; - import Root from './src/Root'; import { name as appName } from './app.json'; import './src/utils/fetch-polyfill'; @@ -22,4 +20,4 @@ if (__DEV__ && !__JEST__ && !__E2E__) { require('./ReactotronConfig'); } -AppRegistry.registerComponent(appName, () => gestureHandlerRootHOC(Root)); +AppRegistry.registerComponent(appName, () => Root); diff --git a/package.json b/package.json index 84ddb8d51..cad645bac 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@react-native-clipboard/clipboard": "1.16.1", "@react-native-community/blur": "4.4.1", "@react-native-community/netinfo": "11.4.1", + "@react-navigation/drawer": "^7.1.1", "@react-navigation/native": "7.0.14", "@react-navigation/native-stack": "7.2.0", "@reduxjs/toolkit": "2.2.6", diff --git a/src/App.tsx b/src/App.tsx index 72bfb1ae5..457d43465 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,7 +31,7 @@ const RecoveryNavigator = lazy( () => import('./screens/Recovery/RecoveryNavigator'), ); const OnboardingNavigator = lazy( - () => import('./navigation/onboarding/OnboardingNavigator'), + () => import('./navigation/OnboardingNavigator'), ); const App = (): ReactElement => { diff --git a/src/AppOnboarded.tsx b/src/AppOnboarded.tsx index ecda94f2f..f91e4cd65 100644 --- a/src/AppOnboarded.tsx +++ b/src/AppOnboarded.tsx @@ -1,124 +1,21 @@ -import NetInfo from '@react-native-community/netinfo'; -import React, { memo, ReactElement, useEffect, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; -import { AppState } from 'react-native'; - +import React, { memo, ReactElement } from 'react'; import InactivityTracker from './components/InactivityTracker'; -import { useAppSelector } from './hooks/redux'; -import RootNavigator from './navigation/root/RootNavigator'; -import { dispatch } from './store/helpers'; -import { - hideBalanceOnOpenSelector, - pinOnLaunchSelector, - pinSelector, -} from './store/reselect/settings'; -import { isOnlineSelector } from './store/reselect/ui'; -import { - selectedNetworkSelector, - selectedWalletSelector, -} from './store/reselect/wallet'; -import { updateSettings } from './store/slices/settings'; -import { updateUi } from './store/slices/ui'; -import { unsubscribeFromLightningSubscriptions } from './utils/lightning'; -import { showToast } from './utils/notifications'; -import { startWalletServices } from './utils/startup'; -import { getOnChainWalletElectrumAsync } from './utils/wallet'; -// import { updateExchangeRates } from './store/actions/wallet'; +import { useAppStateHandler } from './hooks/useAppStateHandler'; +import { useNetworkConnectivity } from './hooks/useNetworkConnectivity'; +import { useWalletStartup } from './hooks/useWalletStartup'; +import DrawerNavigator from './navigation/root/DrawerNavigator'; +import RootNavigationContainer from './navigation/root/RootNavigationContainer'; const AppOnboarded = (): ReactElement => { - const { t } = useTranslation('other'); - const appState = useRef(AppState.currentState); - const selectedWallet = useAppSelector(selectedWalletSelector); - const selectedNetwork = useAppSelector(selectedNetworkSelector); - const hideBalanceOnOpen = useAppSelector(hideBalanceOnOpenSelector); - const pin = useAppSelector(pinSelector); - const pinOnLaunch = useAppSelector(pinOnLaunchSelector); - const isOnline = useAppSelector(isOnlineSelector); - - // on App start - // biome-ignore lint/correctness/useExhaustiveDependencies: onMount - useEffect(() => { - startWalletServices({ selectedNetwork, selectedWallet }); - - const needsAuth = pin && pinOnLaunch; - dispatch(updateUi({ isAuthenticated: !needsAuth })); - - if (hideBalanceOnOpen) { - dispatch(updateSettings({ hideBalance: true })); - } - - return (): void => { - unsubscribeFromLightningSubscriptions(); - }; - }, []); - - // biome-ignore lint/correctness/useExhaustiveDependencies: onMount - useEffect(() => { - // on AppState change - const appStateSubscription = AppState.addEventListener( - 'change', - async (nextAppState) => { - dispatch(updateUi({ appState: nextAppState })); - const electrum = await getOnChainWalletElectrumAsync(); - // on App to foreground - if ( - appState.current.match(/inactive|background/) && - nextAppState === 'active' - ) { - // resubscribe to electrum connection changes - electrum.startConnectionPolling(); - } - - // on App to background - if ( - appState.current.match(/active|inactive/) && - nextAppState === 'background' - ) { - electrum.stopConnectionPolling(); - } - - appState.current = nextAppState; - }, - ); - - return (): void => { - appStateSubscription.remove(); - }; - }, [selectedNetwork]); - - useEffect(() => { - // subscribe to connection information - const unsubscribeNetInfo = NetInfo.addEventListener(({ isConnected }) => { - if (isConnected) { - // prevent toast from showing on startup - if (isOnline !== isConnected) { - showToast({ - type: 'success', - title: t('connection_back_title'), - description: t('connection_back_msg'), - }); - } - dispatch(updateUi({ isOnline: true })); - // FIXME: this runs too often - // updateExchangeRates(); - } else { - showToast({ - type: 'warning', - title: t('connection_issue'), - description: t('connection_issue_explain'), - }); - dispatch(updateUi({ isOnline: false })); - } - }); - - return (): void => { - unsubscribeNetInfo(); - }; - }, [isOnline, t]); + useWalletStartup(); + useAppStateHandler(); + useNetworkConnectivity(); return ( - + + + ); }; diff --git a/src/Root.tsx b/src/Root.tsx index 8d0cb70fb..afdaccca5 100644 --- a/src/Root.tsx +++ b/src/Root.tsx @@ -1,6 +1,7 @@ import { EventEmitter } from 'events'; import React, { ReactElement } from 'react'; import { StyleSheet, View } from 'react-native'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { enableFreeze, enableScreens } from 'react-native-screens'; import { Provider } from 'react-redux'; import { PersistGate } from 'redux-persist/integration/react'; @@ -17,13 +18,15 @@ enableFreeze(true); const Root = (): ReactElement => { return ( - - } - persistor={persistor}> - - - + + + } + persistor={persistor}> + + + + ); }; diff --git a/src/assets/icons/header.ts b/src/assets/icons/header.ts deleted file mode 100644 index 33cd148a8..000000000 --- a/src/assets/icons/header.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const settings = (color = 'white'): string => - ``; - -export const profileIcon = (color = 'white'): string => - ``; diff --git a/src/assets/icons/settings.ts b/src/assets/icons/settings.ts index b5f91d892..e79a4f1c9 100644 --- a/src/assets/icons/settings.ts +++ b/src/assets/icons/settings.ts @@ -1,58 +1,108 @@ -export const chevronRightIcon = ( - color = 'white', -): string => ` - - -`; - -export const rightArrowIcon = (color = 'white'): string => - ``; - -export const upArrowIcon = (color = 'white'): string => - ``; - -export const downArrowIcon = (color = 'white'): string => - ``; - -export const arrowCounterClock = (color = 'white'): string => - ` +export const chevronRightIcon = (color = 'white'): string => ` + + +`; + +export const rightArrowIcon = (color = 'white'): string => ` + + +`; + +export const upArrowIcon = (color = 'white'): string => ` + + + + + +`; + +export const downArrowIcon = (color = 'white'): string => ` + + + + + +`; + +export const arrowCounterClock = (color = 'white'): string => ` + - - `; - -export const checkmarkIcon = (color = 'white'): string => - ``; - -export const copyIcon = (color = 'white'): string => - ``; - -export const faceIdIcon = (color = 'white'): string => - ``; - -export const touchIdIcon = (color = 'white'): string => - ``; - -export const emailIcon = (color = 'white'): string => - ``; - -export const githubIcon = (color = 'white'): string => - ``; - -export const globeIcon = (color = 'white'): string => - ``; - -export const globeSimpleIcon = (color = 'white'): string => - ` +`; + +export const checkmarkIcon = (color = 'white'): string => ` + + +`; + +export const copyIcon = (color = 'white'): string => ` + + + + + + + + +`; + +export const faceIdIcon = (color = 'white'): string => ` + + +`; + +export const touchIdIcon = (color = 'white'): string => ` + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +export const githubIcon = (color = 'white'): string => ` + + +`; + +export const globeIcon = (color = 'white'): string => ` + + + + + + +`; + +export const globeSimpleIcon = (color = 'white'): string => ` + - - `; + `; -export const broadcastIcon = (color = 'white'): string => - ` +export const broadcastIcon = (color = 'white'): string => ` + @@ -62,69 +112,115 @@ export const broadcastIcon = (color = 'white'): string => `; -export const cloudCheckIcon = (color = 'white'): string => - ` +export const cloudCheckIcon = (color = 'white'): string => ` + `; -export const mediumIcon = (color = 'white'): string => - ``; +export const mediumIcon = (color = 'white'): string => ` + + +`; -export const twitterIcon = (color = 'white'): string => - ``; +export const twitterIcon = (color = 'white'): string => ` + + +`; -export const discordIcon = (color = 'white'): string => - ` +export const discordIcon = (color = 'white'): string => ` + `; -export const telegramIcon = (color = 'white'): string => - ``; - -export const listIcon = (color = 'white'): string => - ``; - -export const sortAscendingIcon = (color = 'white'): string => - ``; - -export const leftSign = (color = 'white'): string => - ``; - -export const rightSign = (color = 'white'): string => - ``; - -export const arrowClockwise = (color = 'white'): string => - ``; - -export const rectanglesTwo = (color = 'white'): string => - ``; - -export const lightningHollow = (color = 'white'): string => - ``; - -export const generalSettingsIcon = (color = 'white'): string => - ` +export const telegramIcon = (color = 'white'): string => ` + + +`; + +export const listIcon = (color = 'white'): string => ` + + + + +`; + +export const sortAscendingIcon = (color = 'white'): string => ` + + + + + + + + +`; + +export const leftSign = (color = 'white'): string => ` + + +`; + +export const rightSign = (color = 'white'): string => ` + + +`; + +export const arrowClockwise = (color = 'white'): string => ` + + + + + +`; + +export const arrowsClockwise = (color = 'white'): string => ` + + + + +`; + +export const rectanglesTwo = (color = 'white'): string => ` + + + + + + + + + +`; + +export const lightningHollow = (color = 'white'): string => ` + + + + + +`; + +export const generalSettingsIcon = (color = 'white'): string => ` + - -`; +`; export const securityIcon = (color = 'white'): string => ` - + - -`; +`; export const backupIcon = (color = 'white'): string => ` - + @@ -136,7 +232,7 @@ export const backupIcon = (color = 'white'): string => ` `; export const advancedIcon = (color = 'white'): string => ` - + @@ -153,29 +249,26 @@ export const advancedIcon = (color = 'white'): string => ` `; export const supportIcon = (color = 'white'): string => ` - + - -`; +`; export const aboutIcon = (color = 'white'): string => ` - + - -`; +`; -export const devSettingsIcon = (color = 'white'): string => - ` +export const devSettingsIcon = (color = 'white'): string => ` + - -`; +`; diff --git a/src/assets/icons/wallet.ts b/src/assets/icons/wallet.ts index 057f6b90d..70ddfefa6 100644 --- a/src/assets/icons/wallet.ts +++ b/src/assets/icons/wallet.ts @@ -1,219 +1,325 @@ -export const transferIcon = (color = 'white'): string => ` - - - - - - -`; - -export const unitBitcoinIcon = (color = 'white'): string => ` - - - - -`; +export const arrowLNfunds = (color = 'white'): string => ` + + +`; -export const unitSatoshiIcon = ( - color = 'white', -): string => ` - - +export const backIcon = (color = 'white'): string => ` + + `; -export const unitFiatIcon = (color = 'white'): string => - ` - - - - - - - - - `; +export const backspaceIcon = (color = 'white'): string => ` + + + +`; -export const bitcoinIcon = ( - color = 'white', -): string => ` +export const bitcoinIcon = (color = 'white'): string => ` + `; -export const bitcoinSlantedIcon = ( - color = 'white', -): string => ` - +export const bitcoinSlantedIcon = (color = 'white'): string => ` + + `; -export const bitcoinCircleIcon = (color: string): string => - ` - - - - - - - - - - `; - -export const lightningIcon = ( - color = 'white', -): string => ` - - `; - -export const lightningCircleIcon = (): string => ` - - - - -`; +export const bitcoinCircleIcon = (color: string): string => ` + + + +`; -export const unifiedCircleIcon = (): string => ` - - - - - -`; +export const burgerIcon = (color = 'white'): string => ` + + + + +`; -export const lockIcon = ( - color = 'white', -): string => ` - - - - -`; +export const calendarIcon = (color = 'white'): string => ` + + + + + + +`; -export const sentIcon = ( - color = 'white', -): string => ` - +export const cameraIcon = (color = 'white'): string => ` + + + + `; -export const receivedIcon = ( - color = 'white', -): string => ` - +export const checkCircleIcon = (color = 'white'): string => ` + + + + + + + + `; -export const switchIcon = ( - color = 'white', -): string => ` - +export const clipboardTextIcon = (color = 'white'): string => ` + + + + + + + + + + `; -export const pasteIcon = ( - color = 'white', -): string => ` - - +export const clockIcon = (color = 'white'): string => ` + + + + + + + + `; -export const backIcon = ( - color = 'white', -): string => ` - +export const coinsIcon = (color = 'white'): string => ` + + + + + + + + + + + + + + + + `; -export const coinsIcon = ( - color = 'white', -): string => ` - +export const cornersOut = (color = 'white'): string => ` + + + + + `; -export const userPlusIcon = (color = 'white'): string => - ``; +export const eyeIcon = (color = 'white'): string => ` + + + + +`; -export const userMinusIcon = (color = 'white'): string => - ``; +export const exclamationIcon = (color = 'white'): string => ` + + + +`; -export const gitBranchIcon = (color = 'white'): string => - ``; +export const fingerPrintIcon = (color = 'white'): string => ` + + + + + + +`; -export const noteIcon = (color = 'white'): string => - ``; +export const flashlightIcon = (color = 'white'): string => ` + + + + + +`; -export const calendarIcon = (color = 'white'): string => - ` - - - - - - - `; - -export const checkCircleIcon = (color = 'white'): string => - ``; - -export const clockIcon = (color = 'white'): string => - ``; - -export const hourglassIcon = (color = 'white'): string => - ` - - - - - - `; - -export const hourglassSimpleIcon = (color = 'white'): string => - ` - - - - `; +export const gitBranchIcon = (color = 'white'): string => ` + + + + + + + + + +`; -export const timerIcon = (color = 'white'): string => - ``; +export const heartbeatIcon = (color = 'white'): string => ` + + +`; -export const timerIconAlt = (color = 'white'): string => - ``; +export const hourglassIcon = (color = 'white'): string => ` + + + + + + + `; + +export const hourglassSimpleIcon = (color = 'white'): string => ` + + + + + `; + +export const lightningIcon = (color = 'white'): string => ` + + +`; -export const timerSpeedIcon = (color = 'white'): string => - ``; +export const lightningCircleIcon = (color = 'white'): string => ` + + + + + `; + +export const magnifyingGlassIcon = (color = 'white'): string => ` + + + + + + + + +`; -export const magnifyingGlassIcon = (color = 'white'): string => - ``; +export const minusCircledIcon = (color = 'white'): string => ` + + + + + +`; -export const clipboardTextIcon = (color = 'white'): string => - ``; +export const noteIcon = (color = 'white'): string => ` + + + + + + + + + + + +`; -export const usersIcon = (color = 'white'): string => - ``; +export const pasteIcon = (color = 'white'): string => ` + + + +`; -export const userIcon = (color = 'white'): string => - ``; +export const pencilIcon = (color = 'white'): string => ` + + + + +`; -export const speedFastIcon = (color = 'white'): string => - ``; +export const pictureIcon = (color = 'white'): string => ` + + + + + +`; -export const speedNormalIcon = (color = 'white'): string => - ``; +export const plusIcon = (color = 'white'): string => ` + + + +`; -export const speedSlowIcon = (color = 'white'): string => - ``; +export const plusCircledIcon = (color = 'white'): string => ` + + + + + + +`; -export const xIcon = (color = 'white'): string => - ``; +export const powerIcon = (color = 'white'): string => ` + + + +`; -export const tagIcon = (color = 'white'): string => - ``; +export const qrIcon = (color = 'white'): string => ` + + + + + + + + + + + + +`; -export const shareIosIcon = (color = 'white'): string => - ``; +export const receivedIcon = (color = 'white'): string => ` + + +`; -export const shareAndroidIcon = (color = 'white'): string => - ` +export const scanIcon = (color = 'white'): string => ` + + + + + +`; + +export const sentIcon = (color = 'white'): string => ` + + +`; + +export const settingsIcon = (color = 'white'): string => ` + + + + + + + + +`; + +export const shareIosIcon = (color = 'white'): string => ` + + + + + + +`; + +export const shareAndroidIcon = (color = 'white'): string => ` + @@ -221,211 +327,204 @@ export const shareAndroidIcon = (color = 'white'): string => - - `; +`; -export const penIcon = (color = 'white'): string => - ``; +export const speedFastIcon = (color = 'white'): string => ` + + + + + + +`; -export const pencilIcon = (color = 'white'): string => - ``; +export const speedNormalIcon = (color = 'white'): string => ` + + + + + + +`; -export const infoIcon = (color = 'white'): string => - ``; +export const speedSlowIcon = (color = 'white'): string => ` + + + + + + +`; -export const scanIcon = (color = 'white'): string => ` - - - - - - -`; +export const stackIcon = (color = 'white'): string => ` + + + + + +`; -export const savingsIcon = ( - color = 'white', -): string => ` - - - - - - `; - -export const cameraIcon = (color = 'white'): string => - ``; - -export const trashIcon = (color = 'white'): string => - ``; - -export const plusIcon = (color = 'white'): string => - ``; - -export const cornersOut = (color = 'white'): string => - ``; - -export const pictureIcon = ( - color = 'white', -): string => ` - - - - - `; +export const switchIcon = (color = 'white'): string => ` + + +`; -export const flashlightIcon = ( - color = 'white', -): string => ` - - - - - - `; - -export const brokenLinkIcon = ( - color = 'white', -): string => ` - - - - - - - - - `; - -export const eyeIcon = ( - color = 'white', -): string => ` - - - - `; - -export const heartbeatIcon = ( - color = 'white', -): string => ` - - - `; - -export const chartLineIcon = ( - color = 'white', -): string => ` - - - - `; - -export const newspaperIcon = ( - color = 'white', -): string => ` - - - - - - - - `; - -export const cubeIcon = ( - color = 'white', -): string => ` - - - - - - `; - -export const lightbulbIcon = ( - color = 'white', -): string => ` - - - - - - `; - -export const minusCircledIcon = ( - color = 'white', -): string => ` - - +export const tagIcon = (color = 'white'): string => ` + + + + + - - `; +`; -export const plusCircledIcon = ( - color = 'white', -): string => ` - - +export const timerIcon = (color = 'white'): string => ` + + + + +`; + +export const timerIconAlt = (color = 'white'): string => ` + + +`; + +export const timerSpeedIcon = (color = 'white'): string => ` + + + + + + + - - - `; + +`; -export const qrIcon = ( - color = 'white', -): string => ` - - - - - - - - - - - -`; +export const transferIcon = (color = 'white'): string => ` + + + + + +`; + +export const trashIcon = (color = 'white'): string => ` + + + + + + + +`; -export const questionMarkIcon = - (): string => ` - - - - - +export const unifiedCircleIcon = (): string => ` + + + + `; -export const keyIcon = (color = 'white'): string => - ``; +export const unitBitcoinIcon = (color = 'white'): string => ` + + + +`; + +export const unitSatoshiIcon = (color = 'white'): string => ` + + + +`; + +export const unitFiatIcon = (color = 'white'): string => ` + + + + + + + + + +`; + +export const userIcon = (color = 'white'): string => ` + + + + + + + + +`; -export const backspaceIcon = (color = 'white'): string => - ``; +export const userMinusIcon = (color = 'white'): string => ` + + + + + + + + + +`; -export const exclamationIcon = (color = 'white'): string => - ``; +export const userPlusIcon = (color = 'white'): string => ` + + + + + + + + + + +`; -export const fingerPrintIcon = (color = 'white'): string => - ``; +export const usersIcon = (color = 'white'): string => ` + + + + + + + + + + +`; -export const mapTrifoldIcon = (color = 'white'): string => - ` - - - - +export const userSquareIcon = (color = 'white'): string => ` + + + + + `; -export const mapPinLineIcon = (color = 'white'): string => - ` - - - - +export const warningIcon = (color = 'white'): string => ` + + + + + `; -export const arrowLNfunds = (color = 'white'): string => - ` - - -`; +export const xIcon = (color = 'white'): string => ` + + + + + +`; diff --git a/src/components/AppStatus.tsx b/src/components/AppStatus.tsx new file mode 100644 index 000000000..4f2db9043 --- /dev/null +++ b/src/components/AppStatus.tsx @@ -0,0 +1,135 @@ +import { ReactNode, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PressableProps, StyleSheet } from 'react-native'; +import Animated, { + useAnimatedStyle, + useSharedValue, + withRepeat, + withSequence, + withTiming, + Easing, +} from 'react-native-reanimated'; + +import { __E2E__ } from '../constants/env'; +import { useAppStatus } from '../hooks/useAppStatus'; +import { Pressable } from '../styles/components'; +import { ArrowsClockwiseIcon, PowerIcon, WarningIcon } from '../styles/icons'; +import { BodyMSB } from '../styles/text'; +import { IThemeColors } from '../styles/themes'; + +type Props = PressableProps & { showText?: boolean; showReady?: boolean }; + +const AppStatus = ({ + showText = false, + showReady = false, + style, + testID, + onPress, +}: Props): ReactNode => { + const appStatus = useAppStatus(); + const rotation = useSharedValue(0); + const opacity = useSharedValue(1); + const { t } = useTranslation('wallet'); + + useEffect(() => { + if (__E2E__) { + return; + } + + if (appStatus === 'pending') { + rotation.value = withRepeat( + withSequence( + // First half turn with easing + withTiming(0.5, { + duration: 800, + easing: Easing.bezier(0.4, 0, 0.2, 1), + }), + // Second half turn with different easing + withTiming(1, { + duration: 1200, + easing: Easing.bezier(0.4, 0, 0.2, 1), + }), + ), + -1, + false, + ); + } else { + rotation.value = 0; + } + + if (appStatus === 'error') { + opacity.value = withRepeat( + withSequence( + withTiming(0.3, { + duration: 600, + easing: Easing.ease, + }), + withTiming(1, { + duration: 600, + easing: Easing.ease, + }), + ), + -1, + true, + ); + } else { + opacity.value = 1; + } + }, [appStatus, rotation, opacity]); + + const spinStyle = useAnimatedStyle(() => { + return { + transform: [{ rotate: `${rotation.value * 360}deg` }], + }; + }); + + const fadeStyle = useAnimatedStyle(() => { + return { + opacity: opacity.value, + }; + }); + + const appStatusColor = (): keyof IThemeColors => { + if (appStatus === 'ready') { + return 'green'; + } + if (appStatus === 'pending') { + return 'yellow'; + } + return 'red'; + }; + + const color = appStatusColor(); + + if (appStatus === 'ready' && !showReady) { + return null; + } + + return ( + + {appStatus === 'ready' && ( + + )} + {appStatus === 'pending' && ( + + + + )} + {appStatus === 'error' && ( + + + + )} + {showText && {t('drawer.status')}} + + ); +}; + +const styles = StyleSheet.create({ + root: { + flexDirection: 'row', + alignItems: 'center', + }, +}); + +export default AppStatus; diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx index 828b81cb5..0cc6b47dd 100644 --- a/src/components/TabBar.tsx +++ b/src/components/TabBar.tsx @@ -1,3 +1,4 @@ +import { useNavigation } from '@react-navigation/native'; import React, { ReactElement, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -13,7 +14,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { receiveIcon, sendIcon } from '../assets/icons/tabs'; import useColors from '../hooks/colors'; import { useAppSelector } from '../hooks/redux'; -import { rootNavigation } from '../navigation/root/RootNavigator'; +import { rootNavigation } from '../navigation/root/RootNavigationContainer'; import type { RootNavigationProp } from '../navigation/types'; import { resetSendTransaction } from '../store/actions/wallet'; import { spendingOnboardingSelector } from '../store/reselect/aggregations'; @@ -23,14 +24,11 @@ import { toggleBottomSheet } from '../store/utils/ui'; import { ScanIcon } from '../styles/icons'; import ButtonBlur from './buttons/ButtonBlur'; -const TabBar = ({ - navigation, -}: { - navigation: RootNavigationProp; -}): ReactElement => { +const TabBar = (): ReactElement => { const { white10 } = useColors(); const insets = useSafeAreaInsets(); const { t } = useTranslation('wallet'); + const navigation = useNavigation(); const viewControllers = useAppSelector(viewControllersSelector); const isSpendingOnboarding = useAppSelector(spendingOnboardingSelector); @@ -47,7 +45,7 @@ const TabBar = ({ }, [viewControllers]); const onReceivePress = (): void => { - const currentRoute = rootNavigation.getCurrenRoute(); + const currentRoute = rootNavigation.getCurrentRoute(); // if we are on the spending screen and the user has not yet received funds if (currentRoute === 'ActivitySpending' && isSpendingOnboarding) { diff --git a/src/components/Widgets.tsx b/src/components/Widgets.tsx index 23265a9a7..d87a97bc2 100644 --- a/src/components/Widgets.tsx +++ b/src/components/Widgets.tsx @@ -1,4 +1,5 @@ import { useFocusEffect } from '@react-navigation/native'; +import { useNavigation } from '@react-navigation/native'; import React, { ReactElement, memo, @@ -14,7 +15,7 @@ import DraggableFlatList, { } from 'react-native-draggable-flatlist'; import { useAppDispatch, useAppSelector } from '../hooks/redux'; -import { rootNavigation } from '../navigation/root/RootNavigator'; +import { RootNavigationProp } from '../navigation/types'; import { onboardedWidgetsSelector, widgetsOrderSelector, @@ -42,6 +43,7 @@ import WeatherWidget from './widgets/WeatherWidget'; const Widgets = (): ReactElement => { const { t } = useTranslation('widgets'); const dispatch = useAppDispatch(); + const navigation = useNavigation(); const widgets = useAppSelector(widgetsSelector); const sortOrder = useAppSelector(widgetsOrderSelector); @@ -74,7 +76,7 @@ const Widgets = (): ReactElement => { const screen = onboardedWidgets ? 'WidgetsSuggestions' : 'WidgetsOnboarding'; - rootNavigation.navigate(screen); + navigation.navigate(screen); }; const renderItem = useCallback( diff --git a/src/hooks/useAppStateHandler.ts b/src/hooks/useAppStateHandler.ts new file mode 100644 index 000000000..5cd77c75b --- /dev/null +++ b/src/hooks/useAppStateHandler.ts @@ -0,0 +1,41 @@ +import { useEffect, useRef } from 'react'; +import { AppState } from 'react-native'; +import { dispatch } from '../store/helpers'; +import { updateUi } from '../store/slices/ui'; +import { getOnChainWalletElectrumAsync } from '../utils/wallet'; + +export const useAppStateHandler = (): void => { + const appState = useRef(AppState.currentState); + + useEffect(() => { + const appStateSubscription = AppState.addEventListener( + 'change', + async (nextAppState) => { + dispatch(updateUi({ appState: nextAppState })); + const electrum = await getOnChainWalletElectrumAsync(); + // App to foreground + if ( + appState.current.match(/inactive|background/) && + nextAppState === 'active' + ) { + // resubscribe to electrum connection changes + electrum.startConnectionPolling(); + } + + // App to background + if ( + appState.current.match(/active|inactive/) && + nextAppState === 'background' + ) { + electrum.stopConnectionPolling(); + } + + appState.current = nextAppState; + }, + ); + + return (): void => { + appStateSubscription.remove(); + }; + }, []); +}; diff --git a/src/hooks/useAppStatus.ts b/src/hooks/useAppStatus.ts new file mode 100644 index 000000000..6365938d2 --- /dev/null +++ b/src/hooks/useAppStatus.ts @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react'; +import { appStatusSelector } from '../store/reselect/ui'; +import { THealthState } from '../store/types/ui'; +import { useAppSelector } from './redux'; + +// Give the app some time to initialize before showing the status +const INIT_DELAY = 5000; + +export const useAppStatus = (): THealthState => { + const [showStatus, setShowStatus] = useState(false); + const appStatus = useAppSelector(appStatusSelector); + + useEffect(() => { + const timer = setTimeout(() => { + setShowStatus(true); + }, INIT_DELAY); + + return () => clearTimeout(timer); + }, []); + + // During initialization, return 'ready' instead of error + if (!showStatus) { + return 'ready'; + } + + return appStatus; +}; diff --git a/src/hooks/useNetworkConnectivity.ts b/src/hooks/useNetworkConnectivity.ts new file mode 100644 index 000000000..aceb16e3a --- /dev/null +++ b/src/hooks/useNetworkConnectivity.ts @@ -0,0 +1,42 @@ +import NetInfo from '@react-native-community/netinfo'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { dispatch } from '../store/helpers'; +import { isOnlineSelector } from '../store/reselect/ui'; +import { updateUi } from '../store/slices/ui'; +import { showToast } from '../utils/notifications'; +import { useAppSelector } from './redux'; + +export const useNetworkConnectivity = (): void => { + const { t } = useTranslation('other'); + const isOnline = useAppSelector(isOnlineSelector); + + useEffect(() => { + const unsubscribeNetInfo = NetInfo.addEventListener(({ isConnected }) => { + if (isConnected) { + // prevent toast from showing on startup + if (isOnline !== isConnected) { + showToast({ + type: 'success', + title: t('connection_back_title'), + description: t('connection_back_msg'), + }); + } + dispatch(updateUi({ isOnline: true })); + // FIXME: this runs too often + // updateExchangeRates(); + } else { + showToast({ + type: 'warning', + title: t('connection_issue'), + description: t('connection_issue_explain'), + }); + dispatch(updateUi({ isOnline: false })); + } + }); + + return (): void => { + unsubscribeNetInfo(); + }; + }, [isOnline, t]); +}; diff --git a/src/hooks/useWalletStartup.ts b/src/hooks/useWalletStartup.ts new file mode 100644 index 000000000..49266764d --- /dev/null +++ b/src/hooks/useWalletStartup.ts @@ -0,0 +1,34 @@ +import { useEffect } from 'react'; +import { dispatch } from '../store/helpers'; +import { + hideBalanceOnOpenSelector, + pinOnLaunchSelector, + pinSelector, +} from '../store/reselect/settings'; +import { updateSettings } from '../store/slices/settings'; +import { updateUi } from '../store/slices/ui'; +import { unsubscribeFromLightningSubscriptions } from '../utils/lightning'; +import { startWalletServices } from '../utils/startup'; +import { useAppSelector } from './redux'; + +export const useWalletStartup = (): void => { + const hideBalanceOnOpen = useAppSelector(hideBalanceOnOpenSelector); + const pinEnabled = useAppSelector(pinSelector); + const pinOnLaunch = useAppSelector(pinOnLaunchSelector); + + // biome-ignore lint/correctness/useExhaustiveDependencies: onMount + useEffect(() => { + startWalletServices(); + + const needsAuth = pinEnabled && pinOnLaunch; + dispatch(updateUi({ isAuthenticated: !needsAuth })); + + if (hideBalanceOnOpen) { + dispatch(updateSettings({ hideBalance: true })); + } + + return () => { + unsubscribeFromLightningSubscriptions(); + }; + }, []); +}; diff --git a/src/navigation/onboarding/OnboardingNavigator.tsx b/src/navigation/OnboardingNavigator.tsx similarity index 73% rename from src/navigation/onboarding/OnboardingNavigator.tsx rename to src/navigation/OnboardingNavigator.tsx index bf74b5d60..140d099a5 100644 --- a/src/navigation/onboarding/OnboardingNavigator.tsx +++ b/src/navigation/OnboardingNavigator.tsx @@ -1,19 +1,19 @@ +import { DarkTheme, NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import React, { ReactElement } from 'react'; -import { useAppSelector } from '../../hooks/redux'; +import { useAppSelector } from '../hooks/redux'; import CreateWallet, { TCreateWalletParams, -} from '../../screens/Onboarding/CreateWallet'; -import MultipleDevices from '../../screens/Onboarding/MultipleDevices'; -import Passphrase from '../../screens/Onboarding/Passphrase'; -import RestoreFromSeed from '../../screens/Onboarding/RestoreFromSeed'; -import SlideshowScreen from '../../screens/Onboarding/Slideshow'; -import TermsOfUse from '../../screens/Onboarding/TermsOfUse'; -import WelcomeScreen from '../../screens/Onboarding/Welcome'; -import { requiresRemoteRestoreSelector } from '../../store/reselect/user'; -import { walletExistsSelector } from '../../store/reselect/wallet'; -import { NavigationContainer } from '../../styles/components'; +} from '../screens/Onboarding/CreateWallet'; +import MultipleDevices from '../screens/Onboarding/MultipleDevices'; +import Passphrase from '../screens/Onboarding/Passphrase'; +import RestoreFromSeed from '../screens/Onboarding/RestoreFromSeed'; +import SlideshowScreen from '../screens/Onboarding/Slideshow'; +import TermsOfUse from '../screens/Onboarding/TermsOfUse'; +import WelcomeScreen from '../screens/Onboarding/Welcome'; +import { requiresRemoteRestoreSelector } from '../store/reselect/user'; +import { walletExistsSelector } from '../store/reselect/wallet'; export type OnboardingStackParamList = { TermsOfUse: undefined; @@ -46,7 +46,7 @@ const OnboardingNavigator = (): ReactElement => { walletExists && requiresRemoteRestore ? 'CreateWallet' : 'TermsOfUse'; return ( - + ; @@ -86,11 +86,11 @@ export type SettingsStackParamList = { AddressTypePreference: undefined; DevSettings: undefined; LdkDebug: undefined; - ExportToPhone: undefined; + // ExportToPhone: undefined; ResetAndRestore: undefined; BitcoinNetworkSelection: undefined; LightningNodeInfo: undefined; - Channels?: { showClosed: boolean }; + Channels: { showClosed: boolean } | undefined; ChannelDetails: { channel: TChannel }; CloseConnection: { channelId: string }; TagsSettings: undefined; @@ -155,7 +155,7 @@ const SettingsNavigator = (): ReactElement => { - + {/* */} ; diff --git a/src/navigation/wallet/WalletNavigator.tsx b/src/navigation/WalletNavigator.tsx similarity index 66% rename from src/navigation/wallet/WalletNavigator.tsx rename to src/navigation/WalletNavigator.tsx index 85d968c35..82e3f5708 100644 --- a/src/navigation/wallet/WalletNavigator.tsx +++ b/src/navigation/WalletNavigator.tsx @@ -5,13 +5,12 @@ import { } from '@react-navigation/native-stack'; import React, { ReactElement } from 'react'; -import TabBar from '../../components/TabBar'; -import { __E2E__ } from '../../constants/env'; -import ActivityFiltered from '../../screens/Activity/ActivityFiltered'; -import ActivitySavings from '../../screens/Activity/ActivitySavings'; -import ActivitySpending from '../../screens/Activity/ActivitySpending'; -import Home from '../../screens/Wallets/Home'; -import type { RootStackScreenProps } from '../types'; +import TabBar from '../components/TabBar'; +import { __E2E__ } from '../constants/env'; +import ActivityFiltered from '../screens/Activity/ActivityFiltered'; +import ActivitySavings from '../screens/Activity/ActivitySavings'; +import ActivitySpending from '../screens/Activity/ActivitySpending'; +import Home from '../screens/Wallets/Home'; export type WalletStackParamList = { Home: undefined; @@ -29,9 +28,7 @@ const screenOptions: NativeStackNavigationOptions = { animation: __E2E__ ? 'none' : 'default', }; -const WalletStack = ({ - navigation, -}: RootStackScreenProps<'Wallet'>): ReactElement => { +const WalletStack = (): ReactElement => { return ( <> @@ -42,7 +39,7 @@ const WalletStack = ({ {/* TabBar should be visible on all of the above screens */} - + ); }; diff --git a/src/navigation/bottom-sheet/BackupNavigation.tsx b/src/navigation/bottom-sheet/BackupNavigation.tsx index db0e65faa..19d809ab2 100644 --- a/src/navigation/bottom-sheet/BackupNavigation.tsx +++ b/src/navigation/bottom-sheet/BackupNavigation.tsx @@ -19,7 +19,7 @@ import ShowPassphrase from '../../screens/Settings/Backup/ShowPassphrase'; import Success from '../../screens/Settings/Backup/Success'; import Warning from '../../screens/Settings/Backup/Warning'; import { viewControllerIsOpenSelector } from '../../store/reselect/ui'; -import { NavigationContainer } from '../../styles/components'; +import BottomSheetNavigationContainer from './BottomSheetNavigationContainer'; export type BackupNavigationProp = NativeStackNavigationProp; @@ -51,7 +51,7 @@ const BackupNavigation = (): ReactElement => { return ( - + @@ -65,7 +65,7 @@ const BackupNavigation = (): ReactElement => { - + ); diff --git a/src/navigation/bottom-sheet/BottomSheetNavigationContainer.tsx b/src/navigation/bottom-sheet/BottomSheetNavigationContainer.tsx new file mode 100644 index 000000000..5e613df62 --- /dev/null +++ b/src/navigation/bottom-sheet/BottomSheetNavigationContainer.tsx @@ -0,0 +1,24 @@ +import { + DarkTheme, + NavigationContainer, + NavigationContainerProps, + NavigationContainerRef, +} from '@react-navigation/native'; +import React, { forwardRef, ReactElement } from 'react'; + +const theme = { + ...DarkTheme, + colors: { + ...DarkTheme.colors, + background: 'transparent', + }, +}; + +const BottomSheetNavigationContainer = forwardRef< + NavigationContainerRef, + NavigationContainerProps +>((props, ref): ReactElement => { + return ; +}); + +export default BottomSheetNavigationContainer; diff --git a/src/navigation/bottom-sheet/LNURLWithdrawNavigation.tsx b/src/navigation/bottom-sheet/LNURLWithdrawNavigation.tsx index 6a138a4b7..54c726c8d 100644 --- a/src/navigation/bottom-sheet/LNURLWithdrawNavigation.tsx +++ b/src/navigation/bottom-sheet/LNURLWithdrawNavigation.tsx @@ -17,7 +17,7 @@ import { useAppSelector } from '../../hooks/redux'; import Amount from '../../screens/Wallets/LNURLWithdraw/Amount'; import Confirm from '../../screens/Wallets/LNURLWithdraw/Confirm'; import { viewControllerSelector } from '../../store/reselect/ui'; -import { NavigationContainer } from '../../styles/components'; +import BottomSheetNavigationContainer from './BottomSheetNavigationContainer'; export type LNURLWithdrawNavigationProp = NativeStackNavigationProp; @@ -53,7 +53,7 @@ const LNURLWithdrawNavigation = (): ReactElement => { return ( - + @@ -68,7 +68,7 @@ const LNURLWithdrawNavigation = (): ReactElement => { initialParams={{ wParams, amount: wParams.minWithdrawable }} /> - + ); diff --git a/src/navigation/bottom-sheet/OrangeTicketNavigation.tsx b/src/navigation/bottom-sheet/OrangeTicketNavigation.tsx index 033bf4b5d..f429a8927 100644 --- a/src/navigation/bottom-sheet/OrangeTicketNavigation.tsx +++ b/src/navigation/bottom-sheet/OrangeTicketNavigation.tsx @@ -13,11 +13,6 @@ import React, { useState, } from 'react'; -import ErrorScreen from '../../screens/OrangeTicket/Error'; -import Prize from '../../screens/OrangeTicket/Prize'; -import UsedCard from '../../screens/OrangeTicket/UsedCard'; -import { NavigationContainer } from '../../styles/components'; - import BottomSheetWrapper from '../../components/BottomSheetWrapper'; import { __TREASURE_HUNT_HOST__ } from '../../constants/env'; import { @@ -25,9 +20,13 @@ import { useSnapPoints, } from '../../hooks/bottomSheet'; import { useAppSelector } from '../../hooks/redux'; +import ErrorScreen from '../../screens/OrangeTicket/Error'; +import Prize from '../../screens/OrangeTicket/Prize'; +import UsedCard from '../../screens/OrangeTicket/UsedCard'; import { viewControllerSelector } from '../../store/reselect/ui'; import { getNodeId, waitForLdk } from '../../utils/lightning'; import { showToast } from '../../utils/notifications'; +import BottomSheetNavigationContainer from './BottomSheetNavigationContainer'; export type OrangeTicketNavigationProp = NativeStackNavigationProp; @@ -158,7 +157,7 @@ const OrangeTicket = (): ReactElement => { return ( - + @@ -178,7 +177,7 @@ const OrangeTicket = (): ReactElement => { initialParams={{ errorCode }} /> - + ); diff --git a/src/navigation/bottom-sheet/PINNavigation.tsx b/src/navigation/bottom-sheet/PINNavigation.tsx index 35e9429bb..80d4e57db 100644 --- a/src/navigation/bottom-sheet/PINNavigation.tsx +++ b/src/navigation/bottom-sheet/PINNavigation.tsx @@ -6,17 +6,17 @@ import { } from '@react-navigation/native-stack'; import React, { ReactElement, memo } from 'react'; import { BiometryType } from 'react-native-biometrics'; -import { useAppSelector } from '../../hooks/redux'; import BottomSheetWrapper from '../../components/BottomSheetWrapper'; import { __E2E__ } from '../../constants/env'; import { useSnapPoints } from '../../hooks/bottomSheet'; +import { useAppSelector } from '../../hooks/redux'; import AskForBiometrics from '../../screens/Settings/PIN/AskForBiometrics'; import ChoosePIN from '../../screens/Settings/PIN/ChoosePIN'; import PINPrompt from '../../screens/Settings/PIN/PINPrompt'; import Result from '../../screens/Settings/PIN/Result'; import { viewControllerIsOpenSelector } from '../../store/reselect/ui'; -import { NavigationContainer } from '../../styles/components'; +import BottomSheetNavigationContainer from './BottomSheetNavigationContainer'; export type PinNavigationProp = NativeStackNavigationProp; @@ -43,7 +43,7 @@ const PINNavigation = (): ReactElement => { return ( - + @@ -53,7 +53,7 @@ const PINNavigation = (): ReactElement => { /> - + ); diff --git a/src/navigation/bottom-sheet/ProfileLinkNavigation.tsx b/src/navigation/bottom-sheet/ProfileLinkNavigation.tsx index 108a80f7f..2f9b94296 100644 --- a/src/navigation/bottom-sheet/ProfileLinkNavigation.tsx +++ b/src/navigation/bottom-sheet/ProfileLinkNavigation.tsx @@ -13,7 +13,7 @@ import { useAppSelector } from '../../hooks/redux'; import ProfileLink from '../../screens/Profile/ProfileLink'; import ProfileLinkSuggestions from '../../screens/Profile/ProfileLinkSuggestions'; import { viewControllerIsOpenSelector } from '../../store/reselect/ui'; -import { NavigationContainer } from '../../styles/components'; +import BottomSheetNavigationContainer from './BottomSheetNavigationContainer'; export type ProfileLinkNavigationProp = NativeStackNavigationProp; @@ -39,7 +39,7 @@ const ProfileLinkNavigation = (): ReactElement => { return ( - + { component={ProfileLinkSuggestions} /> - + ); diff --git a/src/navigation/bottom-sheet/ReceiveNavigation.tsx b/src/navigation/bottom-sheet/ReceiveNavigation.tsx index c3e5b1a86..2312bc585 100644 --- a/src/navigation/bottom-sheet/ReceiveNavigation.tsx +++ b/src/navigation/bottom-sheet/ReceiveNavigation.tsx @@ -19,7 +19,7 @@ import ReceiveQR from '../../screens/Wallets/Receive/ReceiveQR'; import Tags from '../../screens/Wallets/Receive/Tags'; import { viewControllerSelector } from '../../store/reselect/ui'; import { resetInvoice } from '../../store/slices/receive'; -import { NavigationContainer } from '../../styles/components'; +import BottomSheetNavigationContainer from './BottomSheetNavigationContainer'; export type ReceiveNavigationProp = NativeStackNavigationProp; @@ -69,7 +69,7 @@ const ReceiveNavigation = (): ReactElement => { onOpen={reset} onClose={reset}> - + @@ -84,7 +84,7 @@ const ReceiveNavigation = (): ReactElement => { - + ); diff --git a/src/navigation/bottom-sheet/SendNavigation.tsx b/src/navigation/bottom-sheet/SendNavigation.tsx index d22425dc1..6d234f2e3 100644 --- a/src/navigation/bottom-sheet/SendNavigation.tsx +++ b/src/navigation/bottom-sheet/SendNavigation.tsx @@ -45,8 +45,8 @@ import { } from '../../store/reselect/wallet'; import { EActivityType } from '../../store/types/activity'; import { updateOnchainFeeEstimates } from '../../store/utils/fees'; -import { NavigationContainer } from '../../styles/components'; import { refreshLdk } from '../../utils/lightning'; +import BottomSheetNavigationContainer from './BottomSheetNavigationContainer'; export type SendNavigationProp = NativeStackNavigationProp; @@ -143,7 +143,9 @@ const SendNavigation = (): ReactElement => { testID="SendSheet" onOpen={onOpen}> - + @@ -178,7 +180,7 @@ const SendNavigation = (): ReactElement => { initialParams={{ pParams, url, amount }} /> - + ); diff --git a/src/navigation/bottom-sheet/TreasureHuntNavigation.tsx b/src/navigation/bottom-sheet/TreasureHuntNavigation.tsx index 6c0dc88b3..00c14ce48 100644 --- a/src/navigation/bottom-sheet/TreasureHuntNavigation.tsx +++ b/src/navigation/bottom-sheet/TreasureHuntNavigation.tsx @@ -24,7 +24,7 @@ import Loading from '../../screens/TreasureHunt/Loading'; import Prize from '../../screens/TreasureHunt/Prize'; import { viewControllerSelector } from '../../store/reselect/ui'; import { addTreasureChest } from '../../store/slices/settings'; -import { NavigationContainer } from '../../styles/components'; +import BottomSheetNavigationContainer from './BottomSheetNavigationContainer'; export type TreasureHuntNavigationProp = NativeStackNavigationProp; @@ -130,7 +130,7 @@ const TreasureHuntNavigation = (): ReactElement => { return ( - + @@ -152,7 +152,7 @@ const TreasureHuntNavigation = (): ReactElement => { /> - + ); diff --git a/src/navigation/root/DrawerContent.tsx b/src/navigation/root/DrawerContent.tsx new file mode 100644 index 000000000..45b86e357 --- /dev/null +++ b/src/navigation/root/DrawerContent.tsx @@ -0,0 +1,147 @@ +import { + DrawerContentComponentProps, + DrawerContentScrollView, +} from '@react-navigation/drawer'; +import { useNavigation } from '@react-navigation/native'; +import React, { ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; +import { StyleSheet, View } from 'react-native'; + +import AppStatus from '../../components/AppStatus'; +import GradientView from '../../components/GradientView'; +import colors from '../../styles/colors'; +import { Pressable } from '../../styles/components'; +import { + CoinsIcon, + HeartbeatIcon, + SettingsIcon, + StackIcon, + UserSquareIcon, + UsersIcon, +} from '../../styles/icons'; +import { DrawerText } from '../../styles/text'; +import { DrawerStackNavigationProp } from './DrawerNavigator'; + +type DrawerItemProps = { + icon: ReactElement; + label: string; + testID?: string; + onPress: () => void; +}; + +const DrawerItem = ({ + icon, + label, + testID, + onPress, +}: DrawerItemProps): ReactElement => ( + + {icon} + {label} + +); + +const DrawerContent = (props: DrawerContentComponentProps): ReactElement => { + const { t } = useTranslation('wallet'); + const navigation = useNavigation(); + + return ( + + + } + label={t('drawer.wallet')} + testID="DrawerWallet" + onPress={() => navigation.navigate('Wallet')} + /> + } + label={t('drawer.activity')} + testID="DrawerActivity" + onPress={() => { + navigation.navigate('Wallet', { screen: 'ActivityFiltered' }); + }} + /> + } + label={t('drawer.contacts')} + testID="DrawerContacts" + onPress={() => navigation.navigate('Contacts')} + /> + } + label={t('drawer.profile')} + testID="DrawerProfile" + onPress={() => navigation.navigate('Profile')} + /> + } + label={t('drawer.widgets')} + testID="DrawerWidgets" + onPress={() => navigation.navigate('WidgetsSuggestions')} + /> + } + label={t('drawer.settings')} + testID="DrawerSettings" + onPress={() => { + navigation.navigate('Settings', { screen: 'MainSettings' }); + }} + /> + + { + navigation.navigate('Settings', { screen: 'AppStatus' }); + }} + /> + + + ); +}; + +const styles = StyleSheet.create({ + drawer: { + flex: 1, + }, + drawerContent: { + flex: 1, + }, + drawerItem: { + borderBottomWidth: 1, + borderBottomColor: colors.white10, + flexDirection: 'row', + alignItems: 'center', + height: 56, + }, + drawerItemIcon: { + width: 24, + height: 24, + marginRight: 12, + alignItems: 'center', + justifyContent: 'center', + }, + drawerItemLabel: { + textTransform: 'uppercase', + }, + appStatus: { + marginTop: 'auto', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + }, +}); + +export default DrawerContent; diff --git a/src/navigation/root/DrawerNavigator.tsx b/src/navigation/root/DrawerNavigator.tsx new file mode 100644 index 000000000..d555eb835 --- /dev/null +++ b/src/navigation/root/DrawerNavigator.tsx @@ -0,0 +1,42 @@ +import { + DrawerNavigationProp, + createDrawerNavigator, +} from '@react-navigation/drawer'; +import { CompositeNavigationProp } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import React, { ReactElement } from 'react'; +import { Platform } from 'react-native'; + +import { RootStackParamList } from '../types'; +import DrawerContent from './DrawerContent'; +import RootNavigator from './RootNavigator'; + +const Drawer = createDrawerNavigator(); + +export type DrawerStackNavigationProp = CompositeNavigationProp< + DrawerNavigationProp<{ RootStack: undefined }>, + NativeStackNavigationProp +>; + +const DrawerNavigator = (): ReactElement => { + const isAndroid = Platform.OS === 'android'; + + return ( + } + screenOptions={{ + headerShown: false, + drawerStyle: { width: 200 }, + drawerPosition: 'right', + drawerType: 'front', + overlayColor: 'rgba(0,0,0,0.6)', + // Swipe is not working properly on Android + // TODO: Fix this + swipeEnabled: !isAndroid, + }}> + + + ); +}; + +export default DrawerNavigator; diff --git a/src/navigation/root/RootNavigationContainer.tsx b/src/navigation/root/RootNavigationContainer.tsx new file mode 100644 index 000000000..a1d365aaa --- /dev/null +++ b/src/navigation/root/RootNavigationContainer.tsx @@ -0,0 +1,77 @@ +import { + DarkTheme, + LinkingOptions, + NavigationContainer, + createNavigationContainerRef, +} from '@react-navigation/native'; +import React, { ReactElement } from 'react'; +import { Linking } from 'react-native'; + +import { processUri } from '../../utils/scanner/scanner'; +import { RootStackParamList } from '../types'; + +export type NavigateScreenArgs = { + [K in keyof RootStackParamList]: undefined extends RootStackParamList[K] + ? [screen: K] | [screen: K, params: RootStackParamList[K]] + : [screen: K, params: RootStackParamList[K]]; +}[keyof RootStackParamList]; + +/** + * Helper function to navigate from outside components. + */ +const navigationRef = createNavigationContainerRef(); +export const rootNavigation = { + getCurrentRoute: (): string | undefined => { + if (navigationRef.isReady()) { + const route = navigationRef.getCurrentRoute(); + return route ? route.name : undefined; + } + return undefined; + }, + navigate: (...args: NavigateScreenArgs): void => { + if (navigationRef.isReady()) { + navigationRef.navigate(...args); + } else { + // Decide what to do if react navigation is not ready + console.log('rootNavigation not ready'); + } + }, + goBack: (): void => { + if (navigationRef.isReady()) { + navigationRef.goBack(); + } + }, +}; + +const RootNavigationContainer = ({ + children, +}: { + children: ReactElement; +}): ReactElement => { + const linking: LinkingOptions = { + prefixes: ['bitkit', 'slash', 'bitcoin', 'lightning'], + subscribe(listener): () => void { + // Deep linking if the app is already open + const subscription = Linking.addEventListener('url', ({ url }): void => { + rootNavigation.navigate('Wallet'); + processUri({ uri: url }); + listener(url); + }); + + return () => { + subscription.remove(); + }; + }, + }; + + return ( + + {children} + + ); +}; + +export default RootNavigationContainer; diff --git a/src/navigation/root/RootNavigator.tsx b/src/navigation/root/RootNavigator.tsx index 0acf5b2f3..a89a01471 100644 --- a/src/navigation/root/RootNavigator.tsx +++ b/src/navigation/root/RootNavigator.tsx @@ -1,8 +1,4 @@ import Clipboard from '@react-native-clipboard/clipboard'; -import { - LinkingOptions, - createNavigationContainerRef, -} from '@react-navigation/native'; import { NativeStackNavigationOptions, createNativeStackNavigator, @@ -41,16 +37,16 @@ import { resetSendTransaction } from '../../store/actions/wallet'; import { getStore } from '../../store/helpers'; import { isAuthenticatedSelector } from '../../store/reselect/ui'; import { updateUi } from '../../store/slices/ui'; -import { NavigationContainer } from '../../styles/components'; import BackupSubscriber from '../../utils/backup/backups-subscriber'; import { checkClipboardData } from '../../utils/clipboard'; import { processUri } from '../../utils/scanner/scanner'; +import SettingsNavigator from '../SettingsNavigator'; +import TransferNavigator from '../TransferNavigator'; +import WalletNavigator from '../WalletNavigator'; import BottomSheetsLazy from '../bottom-sheet/BottomSheetsLazy'; import ForceTransfer from '../bottom-sheet/ForceTransfer'; -import SettingsNavigator from '../settings/SettingsNavigator'; -import TransferNavigator from '../transfer/TransferNavigator'; import type { RootStackParamList } from '../types'; -import WalletNavigator from '../wallet/WalletNavigator'; +import { rootNavigation } from './RootNavigationContainer'; const Stack = createNativeStackNavigator(); @@ -59,41 +55,6 @@ const screenOptions: NativeStackNavigationOptions = { animation: __E2E__ ? 'none' : 'default', }; -/** - * Helper function to navigate from outside components. - */ -export const navigationRef = createNavigationContainerRef(); -export const rootNavigation = { - getCurrenRoute: (): string | undefined => { - if (navigationRef.isReady()) { - const route = navigationRef.getCurrentRoute(); - return route ? route.name : undefined; - } - return undefined; - }, - navigate( - ...args: RouteName extends unknown - ? undefined extends RootStackParamList[RouteName] - ? - | [screen: RouteName] - | [screen: RouteName, params: RootStackParamList[RouteName]] - : [screen: RouteName, params: RootStackParamList[RouteName]] - : never - ): void { - if (navigationRef.isReady()) { - navigationRef.navigate(...args); - } else { - // Decide what to do if react navigation is not ready - console.log('rootNavigation not ready'); - } - }, - goBack(): void { - if (navigationRef.isReady()) { - navigationRef.goBack(); - } - }, -}; - const RootNavigator = (): ReactElement => { const { t } = useTranslation('other'); const appState = useRef(AppState.currentState); @@ -103,28 +64,6 @@ const RootNavigator = (): ReactElement => { const isAuthenticated = useAppSelector(isAuthenticatedSelector); const renderCount = useRenderCount(); - const linking: LinkingOptions<{}> = { - prefixes: ['bitkit', 'slash', 'bitcoin', 'lightning'], - // This is just here to prevent a warning - config: { screens: { Wallet: '' } }, - subscribe(listener): () => void { - // Deep linking if the app is already open - const onReceiveURL = ({ url }: { url: string }): void => { - rootNavigation.navigate('Wallet'); - processUri({ uri: url }); - listener(url); - return; - }; - - // Listen to incoming links from deep linking - const subscription = Linking.addEventListener('url', onReceiveURL); - - return () => { - subscription.remove(); - }; - }, - }; - const checkClipboard = async (): Promise => { const result = await checkClipboardData(); if (result.isOk()) { @@ -194,7 +133,7 @@ const RootNavigator = (): ReactElement => { }, [isAuthenticated]); return ( - + <> @@ -242,7 +181,7 @@ const RootNavigator = (): ReactElement => { {/* Should be above AuthCheck */} - + ); }; diff --git a/src/navigation/types/index.ts b/src/navigation/types/index.ts index a15ede24b..0c8b11d98 100644 --- a/src/navigation/types/index.ts +++ b/src/navigation/types/index.ts @@ -10,6 +10,10 @@ import { import type { RecoveryStackParamList } from '../../screens/Recovery/RecoveryNavigator'; import type { IActivityItem } from '../../store/types/activity'; import type { TWidgetId, TWidgetOptions } from '../../store/types/widgets'; +import type { OnboardingStackParamList } from '../OnboardingNavigator'; +import type { SettingsStackParamList } from '../SettingsNavigator'; +import type { TransferStackParamList } from '../TransferNavigator'; +import type { WalletStackParamList } from '../WalletNavigator'; import type { BackupStackParamList } from '../bottom-sheet/BackupNavigation'; import type { LNURLWithdrawStackParamList } from '../bottom-sheet/LNURLWithdrawNavigation'; import type { OrangeTicketStackParamList } from '../bottom-sheet/OrangeTicketNavigation'; @@ -18,10 +22,6 @@ import type { ProfileLinkStackParamList } from '../bottom-sheet/ProfileLinkNavig import type { ReceiveStackParamList } from '../bottom-sheet/ReceiveNavigation'; import type { SendStackParamList } from '../bottom-sheet/SendNavigation'; import type { TreasureHuntStackParamList } from '../bottom-sheet/TreasureHuntNavigation'; -import type { OnboardingStackParamList } from '../onboarding/OnboardingNavigator'; -import type { SettingsStackParamList } from '../settings/SettingsNavigator'; -import type { TransferStackParamList } from '../transfer/TransferNavigator'; -import type { WalletStackParamList } from '../wallet/WalletNavigator'; // TODO: move all navigation related types here // https://reactnavigation.org/docs/typescript#organizing-types diff --git a/src/screens/Activity/ActivityDetail.tsx b/src/screens/Activity/ActivityDetail.tsx index c8a2e7b87..fdf62f38f 100644 --- a/src/screens/Activity/ActivityDetail.tsx +++ b/src/screens/Activity/ActivityDetail.tsx @@ -41,7 +41,7 @@ import { GitBranchIcon, HourglassIcon, HourglassSimpleIcon, - LightningHollow, + LightningHollowIcon, LightningIcon, ReceiveIcon, SendIcon, @@ -454,7 +454,7 @@ const OnchainActivityDetail = ({ title={t('activity_transfer_to_spending')} value={ - { + + {/* TODO: move these up the tree, causing slow down when navigating */} diff --git a/src/screens/Recovery/RecoveryNavigator.tsx b/src/screens/Recovery/RecoveryNavigator.tsx index 1f79afab4..d591e7eae 100644 --- a/src/screens/Recovery/RecoveryNavigator.tsx +++ b/src/screens/Recovery/RecoveryNavigator.tsx @@ -1,3 +1,4 @@ +import { DarkTheme, NavigationContainer } from '@react-navigation/native'; import { NativeStackNavigationOptions, createNativeStackNavigator, @@ -8,7 +9,6 @@ import AuthCheck from '../../components/AuthCheck'; import { __E2E__ } from '../../constants/env'; import Mnemonic from '../../screens/Recovery/Mnemonic'; import Recovery from '../../screens/Recovery/Recovery'; -import { NavigationContainer } from '../../styles/components'; export type RecoveryStackParamList = { AuthCheck: { onSuccess: () => void }; @@ -25,7 +25,7 @@ const screenOptions: NativeStackNavigationOptions = { const RecoveryNavigator = (): ReactElement => { return ( - + diff --git a/src/screens/Settings/AppStatus/index.tsx b/src/screens/Settings/AppStatus/index.tsx index 5b7922a1c..559aa32bd 100644 --- a/src/screens/Settings/AppStatus/index.tsx +++ b/src/screens/Settings/AppStatus/index.tsx @@ -1,216 +1,195 @@ -import React, { memo, ReactElement, useEffect, useMemo, useState } from 'react'; +import React, { memo, ReactElement, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { StyleSheet, View } from 'react-native'; +import { + Linking, + Platform, + Pressable, + StyleProp, + StyleSheet, + View, + ViewStyle, +} from 'react-native'; import { useAppSelector } from '../../../hooks/redux'; +import { SettingsScreenProps } from '../../../navigation/types'; import { backupSelector } from '../../../store/reselect/backup'; -import { blocktankPaidOrdersFullSelector } from '../../../store/reselect/blocktank'; -import { - openChannelsSelector, - pendingChannelsSelector, -} from '../../../store/reselect/lightning'; import { - isConnectedToElectrumSelector, - isElectrumThrottledSelector, - isLDKReadySelector, - isOnlineSelector, + backupStatusSelector, + channelsStatusSelector, + electrumStatusSelector, + internetStatusSelector, + nodeStatusSelector, } from '../../../store/reselect/ui'; -import { TBackupItem } from '../../../store/types/backup'; -import { EBackupCategory } from '../../../store/utils/backup'; -import { IColors } from '../../../styles/colors'; +import { EBackupCategory } from '../../../store/types/backup'; +import { THealthState } from '../../../store/types/ui'; +import colors, { IColors } from '../../../styles/colors'; import { ScrollView, View as ThemedView } from '../../../styles/components'; import { BitcoinSlantedIcon, BroadcastIcon, CloudCheckIcon, GlobeSimpleIcon, - LightningHollow, + LightningHollowIcon, } from '../../../styles/icons'; import { BodyMSB, CaptionB } from '../../../styles/text'; -import { FAILED_BACKUP_CHECK_TIME } from '../../../utils/backup/backups-subscriber'; import { i18nTime } from '../../../utils/i18n'; import SettingsView from '../SettingsView'; -type TStatusItem = +type TStatusId = | 'internet' - | 'bitcoin_node' + | 'electrum' | 'lightning_node' | 'lightning_connection' - | 'full_backup'; - -type TItemState = 'ready' | 'pending' | 'error'; + | 'backup'; interface IStatusItemProps { + id: TStatusId; Icon: React.FunctionComponent; - item: TStatusItem; - state: TItemState; + state: THealthState; subtitle?: string; + style?: StyleProp; + onPress?: () => void; } const Status = ({ + id, Icon, - item, state, subtitle, + style, + onPress, }: IStatusItemProps): ReactElement => { const { t } = useTranslation('settings'); - const { bg, fg }: { fg: keyof IColors; bg: keyof IColors } = useMemo(() => { - switch (state) { - case 'ready': - return { bg: 'green16', fg: 'green' }; - case 'pending': - return { bg: 'yellow16', fg: 'yellow' }; - case 'error': - return { bg: 'red16', fg: 'red' }; - } - }, [state]); + const { + backgroundColor, + foregroundColor, + }: { foregroundColor: keyof IColors; backgroundColor: keyof IColors } = + React.useMemo(() => { + switch (state) { + case 'ready': + return { backgroundColor: 'green16', foregroundColor: 'green' }; + case 'pending': + return { backgroundColor: 'yellow16', foregroundColor: 'yellow' }; + case 'error': + return { backgroundColor: 'red16', foregroundColor: 'red' }; + } + }, [state]); - subtitle = subtitle || t(`status.${item}.${state}`); + subtitle = subtitle || t(`status.${id}.${state}`); return ( - + - - + + - - {t(`status.${item}.title`)} + + {t(`status.${id}.title`)} {subtitle} - + ); }; -const AppStatus = (): ReactElement => { +const AppStatus = ({ + navigation, +}: SettingsScreenProps<'AppStatus'>): ReactElement => { const { t } = useTranslation('settings'); const { t: tTime } = useTranslation('intl', { i18n: i18nTime }); - const isOnline = useAppSelector(isOnlineSelector); - const isConnectedToElectrum = useAppSelector(isConnectedToElectrumSelector); - const isElectrumThrottled = useAppSelector(isElectrumThrottledSelector); - const isLDKReady = useAppSelector(isLDKReadySelector); - const openChannels = useAppSelector(openChannelsSelector); - const pendingChannels = useAppSelector(pendingChannelsSelector); - const paidOrders = useAppSelector(blocktankPaidOrdersFullSelector); - const backup = useAppSelector(backupSelector); - const [now, setNow] = useState(new Date().getTime()); - - const internetState: TItemState = useMemo(() => { - return isOnline ? 'ready' : 'error'; - }, [isOnline]); - const bitcoinNodeState: TItemState = useMemo(() => { - if (isOnline && !isConnectedToElectrum && !isElectrumThrottled) { - return 'pending'; - } - return isConnectedToElectrum ? 'ready' : 'error'; - }, [isOnline, isConnectedToElectrum, isElectrumThrottled]); - - const lightningNodeState: TItemState = useMemo(() => { - return isOnline && isLDKReady ? 'ready' : 'error'; - }, [isOnline, isLDKReady]); + const internetState = useAppSelector(internetStatusSelector); + const electrumState = useAppSelector(electrumStatusSelector); + const nodeState = useAppSelector(nodeStatusSelector); + const channelsState = useAppSelector(channelsStatusSelector); + const backupState = useAppSelector(backupStatusSelector); + const backup = useAppSelector(backupSelector); - const lightningConnectionState: TItemState = useMemo(() => { - if (!isOnline) { - return 'error'; + const backupSubtitle = useMemo(() => { + if (backupState === 'error') { + return t('status.backup.error'); } - if (openChannels.length > 0) { - return 'ready'; - } - if ( - pendingChannels.length > 0 || - Object.keys(paidOrders.created).length > 0 - ) { - return 'pending'; - } - return 'error'; - }, [ - isOnline, - openChannels.length, - pendingChannels.length, - paidOrders.created, - ]); - - // Keep checking backup status - useEffect(() => { - const timer = setInterval(() => { - setNow(new Date().getTime()); - }, FAILED_BACKUP_CHECK_TIME); - - return (): void => clearInterval(timer); - }, []); - - const isBackupSyncOk = useMemo(() => { - const isSyncOk = (b: TBackupItem): boolean => { - return ( - b.synced > b.required || now - b.required < FAILED_BACKUP_CHECK_TIME - ); - }; - - return Object.values(EBackupCategory).every((key) => { - return isSyncOk(backup[key]); + const syncTimes = Object.values(EBackupCategory).map((key) => { + return backup[key].synced; }); - }, [backup, now]); - - const fullBackupState: { state: TItemState; subtitle?: string } = - useMemo(() => { - if (!isBackupSyncOk) { - return { state: 'error' }; - } - const syncTimes = Object.values(EBackupCategory).map((key) => { - return backup[key].synced; - }); - const max = Math.max(...syncTimes); - const subtitle = tTime('dateTime', { - v: new Date(max), - formatParams: { - v: { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - }, + const max = Math.max(...syncTimes); + return tTime('dateTime', { + v: new Date(max), + formatParams: { + v: { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', }, - }); - return { state: 'ready', subtitle }; - }, [tTime, backup, isBackupSyncOk]); + }, + }); + }, [backup, backupState, t, tTime]); const items: IStatusItemProps[] = [ { + id: 'internet', Icon: GlobeSimpleIcon, - item: 'internet', state: internetState, + onPress: () => { + const goToSettings = (): void => { + Platform.OS === 'ios' + ? Linking.openURL('App-Prefs:Settings') + : Linking.sendIntent('android.settings.SETTINGS'); + }; + goToSettings(); + }, }, { + id: 'electrum', Icon: BitcoinSlantedIcon, - item: 'bitcoin_node', - state: bitcoinNodeState, + state: electrumState, + onPress: () => navigation.navigate('ElectrumConfig'), }, { + id: 'lightning_node', Icon: BroadcastIcon, - item: 'lightning_node', - state: lightningNodeState, + state: nodeState, + onPress: () => navigation.navigate('LightningNodeInfo'), }, { - Icon: LightningHollow, - item: 'lightning_connection', - state: lightningConnectionState, + id: 'lightning_connection', + Icon: LightningHollowIcon, + state: channelsState, + onPress: () => navigation.navigate('Channels'), }, { + id: 'backup', Icon: CloudCheckIcon, - item: 'full_backup', - state: fullBackupState.state, - subtitle: fullBackupState.subtitle, + state: backupState, + subtitle: backupSubtitle, + onPress: () => navigation.navigate('BackupSettings'), }, ]; return ( - {items.map((it) => ( - - ))} + {items.map((item, index) => { + const { id, Icon, state, subtitle } = item; + const isLast = index === items.length - 1; + + return ( + + ); + })} ); @@ -223,8 +202,8 @@ const styles = StyleSheet.create({ status: { marginHorizontal: 16, borderBottomWidth: 1, - borderBottomColor: 'rgba(255, 255, 255, 0.1)', - height: 76, + borderBottomColor: colors.white10, + height: 72, flexDirection: 'row', alignItems: 'center', }, @@ -239,7 +218,7 @@ const styles = StyleSheet.create({ width: 32, height: 32, }, - desc: { + description: { flex: 1, }, }); diff --git a/src/screens/Settings/Backup/ExportToPhone.tsx b/src/screens/Settings/Backup/ExportToPhone.tsx deleted file mode 100644 index 55a1f8f66..000000000 --- a/src/screens/Settings/Backup/ExportToPhone.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import React, { memo, ReactElement, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { StyleSheet } from 'react-native'; -import Share, { ShareOptions } from 'react-native-share'; - -import KeyboardAvoidingView from '../../../components/KeyboardAvoidingView'; -import NavigationHeader from '../../../components/NavigationHeader'; -import SafeAreaInset from '../../../components/SafeAreaInset'; -import Button from '../../../components/buttons/Button'; -import type { SettingsScreenProps } from '../../../navigation/types'; -import { TextInput, View } from '../../../styles/components'; -import { BodyM } from '../../../styles/text'; -import { - cleanupBackupFiles, - createBackupFile, -} from '../../../utils/backup/fileBackup'; -import { showToast } from '../../../utils/notifications'; - -const ExportToPhone = ({ - navigation, -}: SettingsScreenProps<'ExportToPhone'>): ReactElement => { - const { t } = useTranslation('backup'); - const [password, setPassword] = useState(''); - const [isCreating, setIsCreating] = useState(false); - - useEffect(() => { - return (): void => { - cleanupBackupFiles().catch(); - }; - }, []); - - const shareToFiles = async (filePath: string): Promise => { - const shareOptions: ShareOptions = { - title: t('export_share'), - failOnCancel: false, - saveToFiles: true, - urls: [filePath], - }; - - try { - const res = await Share.open(shareOptions); - - if (res.success) { - showToast({ - type: 'success', - title: t('export_success_title'), - description: t('export_success_msg'), - }); - navigation.goBack(); - } - } catch (error) { - if (JSON.stringify(error).indexOf('CANCELLED') < 0) { - showToast({ - type: 'warning', - title: t('export_error_title'), - description: t('export_error_msg'), - }); - } - } - }; - - const onCreateBackup = async (): Promise => { - setIsCreating(true); - - const fileRes = await createBackupFile(password); - - if (fileRes.isErr()) { - console.log(fileRes.error.message); - setIsCreating(false); - showToast({ - type: 'warning', - title: t('export_error_title'), - description: t('export_error_file'), - }); - return; - } - - await shareToFiles(fileRes.value); - - setIsCreating(false); - }; - - return ( - - - - {t('export_text')} - - - - - - - - - ); -}; - -const styles = StyleSheet.create({ - root: { - flex: 1, - }, - content: { - flexGrow: 1, - paddingHorizontal: 16, - }, - textField: { - marginTop: 32, - }, - buttonContainer: { - flexDirection: 'row', - justifyContent: 'center', - marginTop: 'auto', - }, - button: { - flex: 1, - marginTop: 16, - }, -}); - -export default memo(ExportToPhone); diff --git a/src/screens/Settings/Backup/Metadata.tsx b/src/screens/Settings/Backup/Metadata.tsx index 6ca4b91a8..b26fefe4e 100644 --- a/src/screens/Settings/Backup/Metadata.tsx +++ b/src/screens/Settings/Backup/Metadata.tsx @@ -9,7 +9,7 @@ import Button from '../../../components/buttons/Button'; import { useAppDispatch, useAppSelector } from '../../../hooks/redux'; import { backupSelector } from '../../../store/reselect/backup'; import { closeSheet } from '../../../store/slices/ui'; -import { EBackupCategory } from '../../../store/utils/backup'; +import { EBackupCategory } from '../../../store/types/backup'; import { BodyM, BodyS, BodySB } from '../../../styles/text'; import { i18nTime } from '../../../utils/i18n'; diff --git a/src/screens/Settings/BackupSettings/index.tsx b/src/screens/Settings/BackupSettings/index.tsx index 16ea9b9dc..a41de688e 100644 --- a/src/screens/Settings/BackupSettings/index.tsx +++ b/src/screens/Settings/BackupSettings/index.tsx @@ -11,12 +11,12 @@ import { backupSelector } from '../../../store/reselect/backup'; import { lightningBackupSelector } from '../../../store/reselect/lightning'; import { forceBackup } from '../../../store/slices/backup'; import { TBackupItem } from '../../../store/types/backup'; -import { EBackupCategory } from '../../../store/utils/backup'; +import { EBackupCategory } from '../../../store/types/backup'; import { toggleBottomSheet } from '../../../store/utils/ui'; import { ScrollView, View as ThemedView } from '../../../styles/components'; import { ArrowClockwise, - LightningHollow, + LightningHollowIcon, NoteIcon, RectanglesTwo, SettingsIcon, @@ -205,7 +205,7 @@ const BackupSettings = ({ if (lightning) { categories.unshift({ - Icon: LightningHollow, + Icon: LightningHollowIcon, title: t('backup.category_connections'), status: { running: false, diff --git a/src/screens/Wallets/BoostPrompt.tsx b/src/screens/Wallets/BoostPrompt.tsx index 463edf538..c570aa498 100644 --- a/src/screens/Wallets/BoostPrompt.tsx +++ b/src/screens/Wallets/BoostPrompt.tsx @@ -17,7 +17,7 @@ import { } from '../../hooks/bottomSheet'; import { useFeeText } from '../../hooks/fees'; import { useAppDispatch, useAppSelector } from '../../hooks/redux'; -import { rootNavigation } from '../../navigation/root/RootNavigator'; +import { rootNavigation } from '../../navigation/root/RootNavigationContainer'; import { resetSendTransaction } from '../../store/actions/wallet'; import { viewControllerSelector } from '../../store/reselect/ui'; import { transactionSelector } from '../../store/reselect/wallet'; diff --git a/src/screens/Wallets/Header.tsx b/src/screens/Wallets/Header.tsx index ce2211988..eec35dcc7 100644 --- a/src/screens/Wallets/Header.tsx +++ b/src/screens/Wallets/Header.tsx @@ -3,17 +3,18 @@ import React, { memo, ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; +import AppStatus from '../../components/AppStatus'; import ProfileImage from '../../components/ProfileImage'; import VerticalShadow from '../../components/VerticalShadow'; import { useProfile, useSlashtags } from '../../hooks/slashtags'; -import { RootNavigationProp } from '../../navigation/types'; +import { DrawerStackNavigationProp } from '../../navigation/root/DrawerNavigator'; import { Pressable } from '../../styles/components'; -import { ProfileIcon, SettingsIcon } from '../../styles/icons'; +import { BurgerIcon } from '../../styles/icons'; import { Title } from '../../styles/text'; import { truncate } from '../../utils/helpers'; const Header = ({ style }: { style?: StyleProp }): ReactElement => { - const navigation = useNavigation(); + const navigation = useNavigation(); const { t } = useTranslation('slashtags'); const { url } = useSlashtags(); const { profile } = useProfile(url); @@ -22,12 +23,12 @@ const Header = ({ style }: { style?: StyleProp }): ReactElement => { navigation.navigate('Profile'); }; - const openContacts = (): void => { - navigation.navigate('Contacts'); + const openAppStatus = (): void => { + navigation.navigate('Settings', { screen: 'AppStatus' }); }; - const openSettings = (): void => { - navigation.navigate('Settings'); + const openDrawer = (): void => { + navigation.openDrawer(); }; return ( @@ -36,40 +37,35 @@ const Header = ({ style }: { style?: StyleProp }): ReactElement => { + testID="Header" + onPressIn={openProfile}> {profile.name ? ( - {truncate(profile?.name, 20)} + {truncate(profile?.name, 18)} ) : ( {t('your_name_capital')} )} - - - - + testID="HeaderAppStatus" + onPress={openAppStatus} + /> - + testID="HeaderMenu" + onPressIn={openDrawer}> + @@ -88,30 +84,12 @@ const styles = StyleSheet.create({ shadowContainer: { ...StyleSheet.absoluteFillObject, }, - cogIcon: { - alignItems: 'center', - justifyContent: 'center', - minHeight: 32, - paddingLeft: 10, - paddingRight: 16, - }, - profileIcon: { - alignItems: 'center', - justifyContent: 'center', - minHeight: 32, - paddingRight: 10, - }, leftColumn: { flex: 6, flexDirection: 'row', alignItems: 'center', paddingLeft: 16, }, - middleColumn: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, rightColumn: { flex: 1, flexDirection: 'row', @@ -121,8 +99,14 @@ const styles = StyleSheet.create({ profileImage: { marginRight: 16, }, - pressed: { - opacity: 1, + appStatus: { + marginRight: 4, + }, + menuIcon: { + alignItems: 'center', + justifyContent: 'center', + paddingLeft: 10, + paddingRight: 16, }, }); diff --git a/src/screens/Wallets/LNURLPay/Confirm.tsx b/src/screens/Wallets/LNURLPay/Confirm.tsx index 8ed441d98..1f015fb3f 100644 --- a/src/screens/Wallets/LNURLPay/Confirm.tsx +++ b/src/screens/Wallets/LNURLPay/Confirm.tsx @@ -30,7 +30,7 @@ import { addPendingPayment } from '../../../store/slices/lightning'; import { updateMetaTxComment } from '../../../store/slices/metadata'; import { EActivityType } from '../../../store/types/activity'; import { AnimatedView, BottomSheetTextInput } from '../../../styles/components'; -import { Checkmark, LightningHollow } from '../../../styles/icons'; +import { Checkmark, LightningHollowIcon } from '../../../styles/icons'; import { BodySSB, Caption13Up } from '../../../styles/text'; import { FeeText } from '../../../utils/fees'; import { @@ -221,7 +221,7 @@ const LNURLConfirm = ({ title={t('send_fee_and_speed')} value={ <> - - state.ui; - -const viewControllerState = (state: RootState): TUiViewController => { +export const viewControllersSelector = ( + state: RootState, +): TUiViewController => { return state.ui.viewControllers; }; -/** - * Returns all viewController data. - */ -export const viewControllersSelector = createSelector( - [uiState], - (ui): TUiViewController => ui.viewControllers, -); - -/** - * Returns specified viewController data. - * @param {RootState} state - * @param {TViewController} viewController - * @returns {IViewControllerData} - */ -export const viewControllerSelector = createSelector( - [ - viewControllerState, - (_viewControllers, viewController: TViewController): TViewController => { - return viewController; - }, - ], - (viewControllers, viewController): IViewControllerData => { - return viewControllers[viewController]; - }, -); +export const viewControllerSelector = ( + state: RootState, + viewController: TViewController, +): IViewControllerData => { + return state.ui.viewControllers[viewController]; +}; -/** - * Returns boolean on whether a given viewController is open. - * @param {RootState} state - * @param {TViewController} viewController - * @returns {boolean} - */ -export const viewControllerIsOpenSelector = createSelector( - [ - viewControllerState, - (_viewControllers, viewController: TViewController): TViewController => { - return viewController; - }, - ], - (viewControllers, viewController): boolean => { - return viewControllers[viewController].isOpen; - }, -); +export const viewControllerIsOpenSelector = ( + state: RootState, + viewController: TViewController, +): boolean => { + return state.ui.viewControllers[viewController].isOpen; +}; -export const showLaterButtonSelector = createSelector( - [uiState], - (ui) => ui.viewControllers.PINNavigation.showLaterButton, -); +export const showLaterButtonSelector = (state: RootState): boolean => { + return state.ui.viewControllers.PINNavigation.showLaterButton ?? false; +}; -export const profileLinkSelector = createSelector( - [uiState], - (ui) => ui.profileLink, -); +export const profileLinkSelector = (state: RootState): TProfileLink => { + return state.ui.profileLink; +}; -export const isAuthenticatedSelector = createSelector( - [uiState], - (ui) => ui.isAuthenticated, -); +export const isAuthenticatedSelector = (state: RootState): boolean => { + return state.ui.isAuthenticated; +}; -export const isOnlineSelector = createSelector( - [uiState], - (ui): boolean => ui.isOnline, -); +export const isOnlineSelector = (state: RootState): boolean => { + return state.ui.isOnline; +}; -export const isLDKReadySelector = createSelector( - [uiState], - (ui): boolean => ui.isLDKReady, -); +export const isLDKReadySelector = (state: RootState): boolean => { + return state.ui.isLDKReady; +}; -export const isConnectedToElectrumSelector = createSelector( - [uiState], - (ui): boolean => ui.isConnectedToElectrum, -); +export const isConnectedToElectrumSelector = (state: RootState): boolean => { + return state.ui.isConnectedToElectrum; +}; -export const isElectrumThrottledSelector = createSelector( - [uiState], - (ui): boolean => ui.isElectrumThrottled, -); +export const isElectrumThrottledSelector = (state: RootState): boolean => { + return state.ui.isElectrumThrottled; +}; -export const appStateSelector = createSelector([uiState], (ui) => ui.appState); +export const appStateSelector = (state: RootState) => { + return state.ui.appState; +}; -export const availableUpdateSelector = createSelector( - [uiState], - (ui) => ui.availableUpdate, -); +export const availableUpdateSelector = (state: RootState) => { + return state.ui.availableUpdate; +}; -export const criticalUpdateSelector = createSelector( - [uiState], - (ui) => ui.availableUpdate?.critical ?? false, -); +export const criticalUpdateSelector = (state: RootState): boolean => { + return state.ui.availableUpdate?.critical ?? false; +}; -export const timeZoneSelector = createSelector([uiState], (ui) => ui.timeZone); +export const timeZoneSelector = (state: RootState): string => { + return state.ui.timeZone; +}; -export const languageSelector = createSelector([uiState], (ui) => ui.language); +export const languageSelector = (state: RootState): string => { + return state.ui.language; +}; export const sendTransactionSelector = (state: RootState): TSendTransaction => { return state.ui.sendTransaction; }; + +export const internetStatusSelector = (state: RootState): THealthState => { + return state.ui.isOnline ? 'ready' : 'error'; +}; + +export const electrumStatusSelector = (state: RootState): THealthState => { + const { isOnline, isConnectedToElectrum, isElectrumThrottled } = state.ui; + if (isOnline && !isConnectedToElectrum && !isElectrumThrottled) { + return 'pending'; + } + return isConnectedToElectrum ? 'ready' : 'error'; +}; + +export const nodeStatusSelector = (state: RootState): THealthState => { + const { isOnline, isLDKReady } = state.ui; + return isOnline && isLDKReady ? 'ready' : 'error'; +}; + +export const channelsStatusSelector = (state: RootState): THealthState => { + const { isOnline } = state.ui; + const openChannels = openChannelsSelector(state); + const pendingChannels = pendingChannelsSelector(state); + const paidOrders = blocktankPaidOrdersFullSelector(state); + + if (!isOnline) { + return 'error'; + } + if (openChannels.length > 0) { + return 'ready'; + } + if ( + pendingChannels.length > 0 || + Object.keys(paidOrders.created).length > 0 + ) { + return 'pending'; + } + return 'error'; +}; + +export const backupStatusSelector = createSelector( + [backupSelector], + (backup): THealthState => { + const now = new Date().getTime(); + const FAILED_BACKUP_CHECK_TIME = 300000; // 5 minutes in milliseconds + + const isSyncOk = (b: TBackupItem): boolean => { + return ( + b.synced > b.required || now - b.required < FAILED_BACKUP_CHECK_TIME + ); + }; + + const isBackupSyncOk = Object.values(EBackupCategory).every((key) => { + return isSyncOk(backup[key]); + }); + + return isBackupSyncOk ? 'ready' : 'error'; + }, +); + +/** + * Returns a combined status of all app components. + * Returns 'ready' if all components are ready, + * 'pending' if any component is pending and none are in error, + * 'error' if any component is in error state. + * // NOTE: We ignore channels for the global app status + */ +export const appStatusSelector = createSelector( + [ + internetStatusSelector, + electrumStatusSelector, + nodeStatusSelector, + backupStatusSelector, + ], + (internetState, electrumState, nodeState, backupState): THealthState => { + const states = [internetState, electrumState, nodeState, backupState]; + + if (states.some((state) => state === 'error')) { + return 'error'; + } + if (states.some((state) => state === 'pending')) { + return 'pending'; + } + return 'ready'; + }, +); diff --git a/src/store/shapes/backup.ts b/src/store/shapes/backup.ts index 05c4b44a7..3655f98a7 100644 --- a/src/store/shapes/backup.ts +++ b/src/store/shapes/backup.ts @@ -1,5 +1,4 @@ -import { TBackupItem, TBackupState } from '../types/backup'; -import { EBackupCategory } from '../utils/backup'; +import { EBackupCategory, TBackupItem, TBackupState } from '../types/backup'; const item: TBackupItem = { required: Date.now() - 1000, diff --git a/src/store/slices/backup.ts b/src/store/slices/backup.ts index 23e4a1035..7f7c80a86 100644 --- a/src/store/slices/backup.ts +++ b/src/store/slices/backup.ts @@ -2,7 +2,7 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { initialBackupState } from '../shapes/backup'; import { EActivityType } from '../types/activity'; -import { EBackupCategory } from '../utils/backup'; +import { EBackupCategory } from '../types/backup'; import { updateActivityItems } from './activity'; import { addPaidBlocktankOrder, updateBlocktankOrder } from './blocktank'; import { diff --git a/src/store/types/backup.ts b/src/store/types/backup.ts index aca6c32dd..fceeb9d8c 100644 --- a/src/store/types/backup.ts +++ b/src/store/types/backup.ts @@ -1,5 +1,14 @@ import { ENetworks, TAccount } from '@synonymdev/react-native-ldk'; -import { EBackupCategory } from '../utils/backup'; + +export enum EBackupCategory { + wallet = 'bitkit_wallet', + settings = 'bitkit_settings', + widgets = 'bitkit_widgets', + metadata = 'bitkit_metadata', + blocktank = 'bitkit_blocktank_orders', + slashtags = 'bitkit_slashtags_contacts', + ldkActivity = 'bitkit_lightning_activity', +} export type TBackupItem = { running: boolean; diff --git a/src/store/types/ui.ts b/src/store/types/ui.ts index a2a85495a..d48f78e51 100644 --- a/src/store/types/ui.ts +++ b/src/store/types/ui.ts @@ -92,6 +92,8 @@ export type TSendTransaction = { fromAddressViewer?: boolean; }; +export type THealthState = 'ready' | 'pending' | 'error'; + export type TUiState = { appState: AppStateStatus; availableUpdate: TAvailableUpdate | null; diff --git a/src/store/utils/backup.ts b/src/store/utils/backup.ts index 216e67c3e..6f065f0ab 100644 --- a/src/store/utils/backup.ts +++ b/src/store/utils/backup.ts @@ -45,23 +45,13 @@ import { updateWidgets, } from '../slices/widgets'; import { EActivityType } from '../types/activity'; -import { TBackupMetadata } from '../types/backup'; +import { EBackupCategory, TBackupMetadata } from '../types/backup'; import { IBlocktank } from '../types/blocktank'; import { TMetadataState } from '../types/metadata'; import { TSlashtagsState } from '../types/slashtags'; import { IWalletItem, TTransfer } from '../types/wallet'; import { updateOnChainActivityList } from './activity'; -export enum EBackupCategory { - wallet = 'bitkit_wallet', - settings = 'bitkit_settings', - widgets = 'bitkit_widgets', - metadata = 'bitkit_metadata', - blocktank = 'bitkit_blocktank_orders', - slashtags = 'bitkit_slashtags_contacts', - ldkActivity = 'bitkit_lightning_activity', -} - export const performLdkRestore = async ({ backupServerDetails, selectedNetwork = getSelectedNetwork(), diff --git a/src/styles/components.ts b/src/styles/components.ts index cdc3eb31a..76da23e6c 100644 --- a/src/styles/components.ts +++ b/src/styles/components.ts @@ -1,8 +1,4 @@ import { BottomSheetTextInput as _BottomSheetTextInput } from '@gorhom/bottom-sheet'; -import { - DefaultTheme, - NavigationContainer as _NavigationContainer, -} from '@react-navigation/native'; import Color from 'color'; import { ColorValue, @@ -52,22 +48,6 @@ export const Container = styled.View` background-color: ${(props): string => props.theme.colors.background}; `; -export const NavigationContainer = styled(_NavigationContainer).attrs( - (props) => ({ - theme: { - ...DefaultTheme, - colors: { - ...DefaultTheme.colors, - card: 'transparent', - text: props.theme.colors.primary, - background: 'transparent', - primary: 'transparent', - border: 'transparent', - }, - }, - }), -)({}); - export const View = styled.View((props) => ({ backgroundColor: props.color ? props.theme.colors[props.color] @@ -119,7 +99,7 @@ export const Pressable = styled(RNPressable)( (props) => ({ backgroundColor: props.color ? props.theme.colors[props.color] - : props.theme.colors.background, + : 'transparent', opacity: props.disabled ? 0.5 : 1, }), ); diff --git a/src/styles/icons.ts b/src/styles/icons.ts index 18e24a7b9..c4f69c141 100644 --- a/src/styles/icons.ts +++ b/src/styles/icons.ts @@ -1,12 +1,12 @@ import { Platform } from 'react-native'; import { SvgXml } from 'react-native-svg'; -import { profileIcon, settings } from '../assets/icons/header'; import { aboutIcon, advancedIcon, arrowClockwise, arrowCounterClock, + arrowsClockwise, backupIcon, broadcastIcon, checkmarkIcon, @@ -16,7 +16,6 @@ import { devSettingsIcon, discordIcon, downArrowIcon, - emailIcon, faceIdIcon, generalSettingsIcon, githubIcon, @@ -43,16 +42,14 @@ import { backspaceIcon, bitcoinCircleIcon, bitcoinSlantedIcon, - brokenLinkIcon, + burgerIcon, calendarIcon, cameraIcon, - chartLineIcon, checkCircleIcon, clipboardTextIcon, clockIcon, coinsIcon, cornersOut, - cubeIcon, exclamationIcon, eyeIcon, fingerPrintIcon, @@ -61,34 +58,27 @@ import { heartbeatIcon, hourglassIcon, hourglassSimpleIcon, - infoIcon, - keyIcon, - lightbulbIcon, lightningCircleIcon, lightningIcon, - lockIcon, magnifyingGlassIcon, - mapPinLineIcon, - mapTrifoldIcon, minusCircledIcon, - newspaperIcon, noteIcon, - penIcon, pencilIcon, pictureIcon, plusCircledIcon, plusIcon, + powerIcon, qrIcon, - questionMarkIcon, receivedIcon, - savingsIcon, scanIcon, sentIcon, + settingsIcon, shareAndroidIcon, shareIosIcon, speedFastIcon, speedNormalIcon, speedSlowIcon, + stackIcon, switchIcon, tagIcon, timerIcon, @@ -102,7 +92,9 @@ import { userIcon, userMinusIcon, userPlusIcon, + userSquareIcon, usersIcon, + warningIcon, xIcon, } from '../assets/icons/wallet'; import styled from './styled-components'; @@ -112,6 +104,12 @@ type IconProps = { color?: keyof IThemeColors; }; +export const BurgerIcon = styled(SvgXml).attrs((props) => ({ + xml: burgerIcon(), + height: props.height ?? '24px', + width: props.width ?? '24px', +}))(() => ({})); + export const ScanIcon = styled(SvgXml).attrs((props) => ({ xml: scanIcon(props.color ? props.theme.colors[props.color] : 'white'), height: props.height ?? '32px', @@ -121,7 +119,15 @@ export const ScanIcon = styled(SvgXml).attrs((props) => ({ })); export const SettingsIcon = styled(SvgXml).attrs((props) => ({ - xml: settings(props.color ? props.theme.colors[props.color] : 'white'), + xml: settingsIcon(props.color ? props.theme.colors[props.color] : 'white'), + height: props.height ?? '24px', + width: props.width ?? '24px', +}))((props) => ({ + color: props.color ? props.theme.colors[props.color] : 'white', +})); + +export const StackIcon = styled(SvgXml).attrs((props) => ({ + xml: stackIcon(props.color ? props.theme.colors[props.color] : 'white'), height: props.height ?? '24px', width: props.width ?? '24px', }))((props) => ({ @@ -339,14 +345,6 @@ export const ShareAndroidIcon = styled(SvgXml).attrs((props) => ({ export const ShareIcon = Platform.OS === 'ios' ? ShareIosIcon : ShareAndroidIcon; -export const PenIcon = styled(SvgXml).attrs((props) => ({ - xml: penIcon(props.color ? props.theme.colors[props.color] : 'white'), - height: props.height ?? '32px', - width: props.width ?? '32px', -}))((props) => ({ - color: props.color ? props.theme.colors[props.color] : 'white', -})); - export const PencilIcon = styled(SvgXml).attrs((props) => ({ xml: pencilIcon(props.color ? props.theme.colors[props.color] : 'white'), height: props.height ?? '32px', @@ -355,14 +353,6 @@ export const PencilIcon = styled(SvgXml).attrs((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); -export const SavingsIcon = styled(SvgXml).attrs((props) => ({ - xml: savingsIcon(props.color ? props.theme.colors[props.color] : 'white'), - height: props.height ?? '32px', - width: props.width ?? '32px', -}))((props) => ({ - color: props.color ? props.theme.colors[props.color] : 'white', -})); - export const BitcoinSlantedIcon = styled(SvgXml).attrs((props) => ({ xml: bitcoinSlantedIcon( props.color ? props.theme.colors[props.color] : 'white', @@ -447,14 +437,6 @@ export const LightningIcon = styled(SvgXml).attrs((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); -export const LockIcon = styled(SvgXml).attrs((props) => ({ - xml: lockIcon(props.color ? props.theme.colors[props.color] : 'white'), - height: props.height ?? '16px', - width: props.width ?? '16px', -}))((props) => ({ - color: props.color ? props.theme.colors[props.color] : 'white', -})); - export const SendIcon = styled(SvgXml).attrs((props) => ({ xml: sentIcon(props.color ? props.theme.colors[props.color] : 'white'), height: props.height ?? '17px', @@ -580,14 +562,6 @@ export const SwitchIcon = styled(SvgXml).attrs((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); -export const ProfileIcon = styled(SvgXml).attrs((props) => ({ - xml: profileIcon(props.color ? props.theme.colors[props.color] : 'white'), - height: props.height ?? '24px', - width: props.width ?? '24px', -}))((props) => ({ - color: props.color ? props.theme.colors[props.color] : 'white', -})); - export const ListIcon = styled(SvgXml).attrs((props) => ({ xml: listIcon( props.color @@ -610,14 +584,6 @@ export const SortAscendingIcon = styled(SvgXml).attrs((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); -export const InfoIcon = styled(SvgXml).attrs((props) => ({ - xml: infoIcon(props.color ? props.theme.colors[props.color] : 'white'), - height: props.height ?? '32px', - width: props.width ?? '32px', -}))((props) => ({ - color: props.color ? props.theme.colors[props.color] : 'white', -})); - export const CameraIcon = styled(SvgXml).attrs((props) => ({ xml: cameraIcon(props.color ? props.theme.colors[props.color] : 'white'), height: props.height ?? '32px', @@ -658,16 +624,6 @@ export const CornersOutIcon = styled(SvgXml).attrs((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); -export const EmailIcon = styled(SvgXml).attrs((props) => ({ - xml: emailIcon( - props.color ? props.theme.colors[props.color] : props.theme.colors.brand, - ), - height: props.height ?? '24px', - width: props.width ?? '24px', -}))((props) => ({ - color: props.color ? props.theme.colors[props.color] : 'white', -})); - export const GithubIcon = styled(SvgXml).attrs((props) => ({ xml: githubIcon( props.color ? props.theme.colors[props.color] : props.theme.colors.brand, @@ -774,16 +730,6 @@ export const FlashlightIcon = styled(SvgXml).attrs((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); -export const BrokenLinkIcon = styled(SvgXml).attrs((props) => ({ - xml: brokenLinkIcon( - props.color ? props.theme.colors[props.color] : props.theme.colors.brand, - ), - height: props.height ?? '32px', - width: props.width ?? '32px', -}))((props) => ({ - color: props.color ? props.theme.colors[props.color] : 'white', -})); - export const EyeIcon = styled(SvgXml).attrs((props) => ({ xml: eyeIcon(props.color ? props.theme.colors[props.color] : 'white'), height: props.height ?? '24px', @@ -818,35 +764,6 @@ export const PlusCircledIcon = styled(SvgXml).attrs((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); -export const KeyIcon = styled(SvgXml).attrs((props) => ({ - xml: keyIcon(props.color ? props.theme.colors[props.color] : 'white'), - height: props.height ?? '24px', - width: props.width ?? '24px', -}))((props) => ({ - color: props.color ? props.theme.colors[props.color] : 'white', -})); - -// Widget icons -export const ChartLineIcon = styled(SvgXml).attrs((props) => ({ - xml: chartLineIcon( - props.color ? props.theme.colors[props.color] : props.theme.colors.brand, - ), - height: props.height ?? '64px', - width: props.width ?? '64px', -}))((props) => ({ - color: props.color ? props.theme.colors[props.color] : 'white', -})); - -export const NewspaperIcon = styled(SvgXml).attrs((props) => ({ - xml: newspaperIcon( - props.color ? props.theme.colors[props.color] : props.theme.colors.brand, - ), - height: props.height ?? '64px', - width: props.width ?? '64px', -}))((props) => ({ - color: props.color ? props.theme.colors[props.color] : 'white', -})); - export const QrIcon = styled(SvgXml).attrs((props) => ({ xml: qrIcon( props.color ? props.theme.colors[props.color] : props.theme.colors.brand, @@ -857,32 +774,6 @@ export const QrIcon = styled(SvgXml).attrs((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); -export const QuestionMarkIcon = styled(SvgXml).attrs((props) => ({ - xml: questionMarkIcon(), - height: props.height ?? '30px', - width: props.width ?? '30px', -}))(() => ({})); - -export const CubeIcon = styled(SvgXml).attrs((props) => ({ - xml: cubeIcon( - props.color ? props.theme.colors[props.color] : props.theme.colors.brand, - ), - height: props.height ?? '64px', - width: props.width ?? '64px', -}))((props) => ({ - color: props.color ? props.theme.colors[props.color] : 'white', -})); - -export const LightBulbIcon = styled(SvgXml).attrs((props) => ({ - xml: lightbulbIcon( - props.color ? props.theme.colors[props.color] : props.theme.colors.brand, - ), - height: props.height ?? '64px', - width: props.width ?? '64px', -}))((props) => ({ - color: props.color ? props.theme.colors[props.color] : 'white', -})); - export const LeftSign = styled(SvgXml).attrs((props) => ({ xml: leftSign( props.color ? props.theme.colors[props.color] : props.theme.colors.white, @@ -913,8 +804,8 @@ export const ArrowClockwise = styled(SvgXml).attrs((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); -export const RectanglesTwo = styled(SvgXml).attrs((props) => ({ - xml: rectanglesTwo( +export const ArrowsClockwiseIcon = styled(SvgXml).attrs((props) => ({ + xml: arrowsClockwise( props.color ? props.theme.colors[props.color] : props.theme.colors.white, ), height: props.height ?? '24px', @@ -923,8 +814,8 @@ export const RectanglesTwo = styled(SvgXml).attrs((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); -export const LightningHollow = styled(SvgXml).attrs((props) => ({ - xml: lightningHollow( +export const RectanglesTwo = styled(SvgXml).attrs((props) => ({ + xml: rectanglesTwo( props.color ? props.theme.colors[props.color] : props.theme.colors.white, ), height: props.height ?? '24px', @@ -933,22 +824,12 @@ export const LightningHollow = styled(SvgXml).attrs((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); -export const MapTrifoldIcon = styled(SvgXml).attrs((props) => ({ - xml: mapTrifoldIcon( - props.color ? props.theme.colors[props.color] : props.theme.colors.white, - ), - height: props.height ?? '16px', - width: props.width ?? '16px', -}))((props) => ({ - color: props.color ? props.theme.colors[props.color] : 'white', -})); - -export const MapPinLineIcon = styled(SvgXml).attrs((props) => ({ - xml: mapPinLineIcon( +export const LightningHollowIcon = styled(SvgXml).attrs((props) => ({ + xml: lightningHollow( props.color ? props.theme.colors[props.color] : props.theme.colors.white, ), - height: props.height ?? '16px', - width: props.width ?? '16px', + height: props.height ?? '24px', + width: props.width ?? '24px', }))((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); @@ -1010,3 +891,27 @@ export const DevSettingsIcon = styled(SvgXml).attrs((props) => ({ }))((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); + +export const PowerIcon = styled(SvgXml).attrs((props) => ({ + xml: powerIcon(props.color ? props.theme.colors[props.color] : 'white'), + height: props.height ?? '24px', + width: props.width ?? '24px', +}))((props) => ({ + color: props.color ? props.theme.colors[props.color] : 'white', +})); + +export const UserSquareIcon = styled(SvgXml).attrs((props) => ({ + xml: userSquareIcon(props.color ? props.theme.colors[props.color] : 'white'), + height: props.height ?? '24px', + width: props.width ?? '24px', +}))((props) => ({ + color: props.color ? props.theme.colors[props.color] : 'white', +})); + +export const WarningIcon = styled(SvgXml).attrs((props) => ({ + xml: warningIcon(props.color ? props.theme.colors[props.color] : 'white'), + height: props.height ?? '24px', + width: props.width ?? '24px', +}))((props) => ({ + color: props.color ? props.theme.colors[props.color] : 'white', +})); diff --git a/src/styles/text.ts b/src/styles/text.ts index a35874f45..48be7a218 100644 --- a/src/styles/text.ts +++ b/src/styles/text.ts @@ -39,6 +39,13 @@ export const Headline = styled.Text( }), ); +export const DrawerText = styled.Text(({ theme, color }) => ({ + ...theme.fonts.black, + fontSize: '24px', + color: theme.colors[color ?? 'primary'], + letterSpacing: -1, +})); + export const Title = styled.Text( ({ theme, color, lineHeight = 26 }) => ({ ...theme.fonts.bold, diff --git a/src/styles/themes.ts b/src/styles/themes.ts index 590d57dc7..25e712328 100644 --- a/src/styles/themes.ts +++ b/src/styles/themes.ts @@ -14,8 +14,6 @@ export interface IThemeColors extends IDefaultColors { primary: string; secondary: string; background: string; - surface: string; - onBackground: string; onSurface: string; refreshControl: string; } @@ -69,8 +67,6 @@ const light: ITheme = { primary: '#121212', secondary: '#121212', background: colors.white80, - surface: '#E8E8E8', - onBackground: '#121212', onSurface: '#D6D6D6', refreshControl: '#121212', }, @@ -84,8 +80,6 @@ const dark: ITheme = { primary: colors.white, secondary: colors.white64, background: colors.black, - surface: '#101010', - onBackground: '#FFFFFF', onSurface: colors.gray6, refreshControl: '#FFFFFF', }, diff --git a/src/utils/backup/backups-subscriber.tsx b/src/utils/backup/backups-subscriber.tsx index 2906555db..51e0738ad 100644 --- a/src/utils/backup/backups-subscriber.tsx +++ b/src/utils/backup/backups-subscriber.tsx @@ -5,7 +5,8 @@ import { __E2E__ } from '../../constants/env'; import { useDebouncedEffect } from '../../hooks/helpers'; import { useAppSelector } from '../../hooks/redux'; import { backupSelector } from '../../store/reselect/backup'; -import { EBackupCategory, performBackup } from '../../store/utils/backup'; +import { EBackupCategory } from '../../store/types/backup'; +import { performBackup } from '../../store/utils/backup'; import { showToast } from '../notifications'; const BACKUP_DEBOUNCE = 5000; // 5 seconds diff --git a/src/utils/i18n/locales/ca/settings.json b/src/utils/i18n/locales/ca/settings.json index 75b359789..80ca2fed5 100644 --- a/src/utils/i18n/locales/ca/settings.json +++ b/src/utils/i18n/locales/ca/settings.json @@ -206,7 +206,7 @@ "string": "Desconectat" } }, - "bitcoin_node": { + "electrum": { "title": { "string": "Node Bitcoin" }, diff --git a/src/utils/i18n/locales/cs/settings.json b/src/utils/i18n/locales/cs/settings.json index a120b6df5..d20827079 100644 --- a/src/utils/i18n/locales/cs/settings.json +++ b/src/utils/i18n/locales/cs/settings.json @@ -329,7 +329,7 @@ "string": "Odpojeno" } }, - "bitcoin_node": { + "electrum": { "title": { "string": "Bitcoinový uzel" }, @@ -371,7 +371,7 @@ "string": "Žádná otevřená spojení" } }, - "full_backup": { + "backup": { "title": { "string": "Nejnovější úplná záloha dat" }, diff --git a/src/utils/i18n/locales/de/settings.json b/src/utils/i18n/locales/de/settings.json index 5ac581de8..3b69309a7 100644 --- a/src/utils/i18n/locales/de/settings.json +++ b/src/utils/i18n/locales/de/settings.json @@ -329,7 +329,7 @@ "string": "Nicht verbunden" } }, - "bitcoin_node": { + "electrum": { "title": { "string": "Bitcoin Node" }, @@ -371,7 +371,7 @@ "string": "Keine offenen Verbindungen" } }, - "full_backup": { + "backup": { "title": { "string": "Vollständiges Backup" }, diff --git a/src/utils/i18n/locales/en/settings.json b/src/utils/i18n/locales/en/settings.json index 4efe685fe..18a89ced4 100644 --- a/src/utils/i18n/locales/en/settings.json +++ b/src/utils/i18n/locales/en/settings.json @@ -329,7 +329,7 @@ "string": "Disconnected" } }, - "bitcoin_node": { + "electrum": { "title": { "string": "Bitcoin Node" }, @@ -371,7 +371,7 @@ "string": "No open connections" } }, - "full_backup": { + "backup": { "title": { "string": "Latest Full Data Backup" }, diff --git a/src/utils/i18n/locales/en/wallet.json b/src/utils/i18n/locales/en/wallet.json index f5acc1fcd..fc8d557f5 100644 --- a/src/utils/i18n/locales/en/wallet.json +++ b/src/utils/i18n/locales/en/wallet.json @@ -1,4 +1,27 @@ { + "drawer": { + "wallet": { + "string": "Wallet" + }, + "activity": { + "string": "Activity" + }, + "contacts": { + "string": "Contacts" + }, + "profile": { + "string": "Profile" + }, + "widgets": { + "string": "Widgets" + }, + "settings": { + "string": "Settings" + }, + "status": { + "string": "App Status" + } + }, "send": { "string": "Send" }, diff --git a/src/utils/i18n/locales/es_419/settings.json b/src/utils/i18n/locales/es_419/settings.json index 424758887..b18108451 100644 --- a/src/utils/i18n/locales/es_419/settings.json +++ b/src/utils/i18n/locales/es_419/settings.json @@ -179,7 +179,7 @@ "string": "Conectado" } }, - "bitcoin_node": { + "electrum": { "ready": { "string": "Conectado" } diff --git a/src/utils/i18n/locales/es_ES/settings.json b/src/utils/i18n/locales/es_ES/settings.json index ee226b732..13d3e1b11 100644 --- a/src/utils/i18n/locales/es_ES/settings.json +++ b/src/utils/i18n/locales/es_ES/settings.json @@ -254,7 +254,7 @@ "string": "Desconectado" } }, - "bitcoin_node": { + "electrum": { "title": { "string": "Nodo Bitcoin" }, @@ -284,7 +284,7 @@ "string": "Abriendo..." } }, - "full_backup": { + "backup": { "title": { "string": "Último Backup de Datos Completo" }, diff --git a/src/utils/i18n/locales/fr/settings.json b/src/utils/i18n/locales/fr/settings.json index fb5bc738d..9dd60a29d 100644 --- a/src/utils/i18n/locales/fr/settings.json +++ b/src/utils/i18n/locales/fr/settings.json @@ -293,7 +293,7 @@ "string": "Déconnecté" } }, - "bitcoin_node": { + "electrum": { "title": { "string": "Nœud Bitcoin" }, @@ -335,7 +335,7 @@ "string": "Pas de connexions ouvertes" } }, - "full_backup": { + "backup": { "title": { "string": "Dernière sauvegarde complète" }, diff --git a/src/utils/i18n/locales/it/settings.json b/src/utils/i18n/locales/it/settings.json index f6ce380f4..ae900ffe7 100644 --- a/src/utils/i18n/locales/it/settings.json +++ b/src/utils/i18n/locales/it/settings.json @@ -296,7 +296,7 @@ "string": "Disconnesso" } }, - "bitcoin_node": { + "electrum": { "title": { "string": "Nodo Bitcoin" }, @@ -338,7 +338,7 @@ "string": "Nessuna connessione aperta" } }, - "full_backup": { + "backup": { "title": { "string": "Ultimo backup completo dei dati" }, diff --git a/src/utils/i18n/locales/nl/settings.json b/src/utils/i18n/locales/nl/settings.json index e51d1dbb8..a16aa160f 100644 --- a/src/utils/i18n/locales/nl/settings.json +++ b/src/utils/i18n/locales/nl/settings.json @@ -296,7 +296,7 @@ "string": "Ontkoppeld" } }, - "bitcoin_node": { + "electrum": { "title": { "string": "Bitcoin-node" }, @@ -338,7 +338,7 @@ "string": "Geen open verbindingen" } }, - "full_backup": { + "backup": { "title": { "string": "Laatste volledige gegevensback-up" }, diff --git a/src/utils/i18n/locales/pt_BR/settings.json b/src/utils/i18n/locales/pt_BR/settings.json index f6bfc8f4d..680ac6cb7 100644 --- a/src/utils/i18n/locales/pt_BR/settings.json +++ b/src/utils/i18n/locales/pt_BR/settings.json @@ -293,7 +293,7 @@ "string": "Desconectado" } }, - "bitcoin_node": { + "electrum": { "title": { "string": "Nó de Bitcoin" }, @@ -335,7 +335,7 @@ "string": "Nenhuma conexão ativa" } }, - "full_backup": { + "backup": { "title": { "string": "Último Backup Completo" }, diff --git a/src/utils/i18n/locales/ru/settings.json b/src/utils/i18n/locales/ru/settings.json index 1081b1f00..a93cee6ae 100644 --- a/src/utils/i18n/locales/ru/settings.json +++ b/src/utils/i18n/locales/ru/settings.json @@ -293,7 +293,7 @@ "string": "Отключено" } }, - "bitcoin_node": { + "electrum": { "title": { "string": "Биткойн-нода" }, @@ -335,7 +335,7 @@ "string": "Нет открытых соединений" } }, - "full_backup": { + "backup": { "title": { "string": "Последнее Полное Резервное Копирование Данных" }, diff --git a/src/utils/scanner/scanner.ts b/src/utils/scanner/scanner.ts index 1fb95dfab..0114ca728 100644 --- a/src/utils/scanner/scanner.ts +++ b/src/utils/scanner/scanner.ts @@ -20,7 +20,7 @@ import { import URLParse from 'url-parse'; import { sendNavigation } from '../../navigation/bottom-sheet/SendNavigation'; -import { rootNavigation } from '../../navigation/root/RootNavigator'; +import { rootNavigation } from '../../navigation/root/RootNavigationContainer'; import { resetSendTransaction, setupOnChainTransaction, diff --git a/src/utils/slashtags/index.ts b/src/utils/slashtags/index.ts index 26a8dcc7c..eff99f1bb 100644 --- a/src/utils/slashtags/index.ts +++ b/src/utils/slashtags/index.ts @@ -4,7 +4,7 @@ import { format, parse } from '@synonymdev/slashtags-url'; import debounce from 'lodash/debounce'; import { webRelayClient } from '../../components/SlashtagsProvider'; -import { rootNavigation } from '../../navigation/root/RootNavigator'; +import { rootNavigation } from '../../navigation/root/RootNavigationContainer'; import { dispatch, getSettingsStore } from '../../store/helpers'; import { updateSettings } from '../../store/slices/settings'; import { diff --git a/src/utils/startup/index.ts b/src/utils/startup/index.ts index 0f2f48c26..6db586bec 100644 --- a/src/utils/startup/index.ts +++ b/src/utils/startup/index.ts @@ -98,7 +98,7 @@ export const startWalletServices = async ({ staleBackupRecoveryMode?: boolean; selectedWallet?: TWalletName; selectedNetwork?: EAvailableNetwork; -}): Promise> => { +} = {}): Promise> => { try { // wait for interactions/animations to be completed await new Promise((resolve) => { diff --git a/src/utils/wallet/transactions.ts b/src/utils/wallet/transactions.ts index fe822a57f..7c50f774e 100644 --- a/src/utils/wallet/transactions.ts +++ b/src/utils/wallet/transactions.ts @@ -22,8 +22,8 @@ import { } from '../../store/helpers'; import { removeActivityItem } from '../../store/slices/activity'; import { requireBackup } from '../../store/slices/backup'; +import { EBackupCategory } from '../../store/types/backup'; import { ETransactionSpeed } from '../../store/types/settings'; -import { EBackupCategory } from '../../store/utils/backup'; import { reduceValue } from '../helpers'; import i18n from '../i18n'; import { EAvailableNetwork } from '../networks'; diff --git a/yarn.lock b/yarn.lock index a05fd174b..c8fa9cbe9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4245,6 +4245,26 @@ __metadata: languageName: node linkType: hard +"@react-navigation/drawer@npm:^7.1.1": + version: 7.1.1 + resolution: "@react-navigation/drawer@npm:7.1.1" + dependencies: + "@react-navigation/elements": ^2.2.5 + color: ^4.2.3 + react-native-drawer-layout: ^4.1.1 + use-latest-callback: ^0.2.1 + peerDependencies: + "@react-navigation/native": ^7.0.14 + react: ">= 18.2.0" + react-native: "*" + react-native-gesture-handler: ">= 2.0.0" + react-native-reanimated: ">= 2.0.0" + react-native-safe-area-context: ">= 4.0.0" + react-native-screens: ">= 4.0.0" + checksum: d849fa23ec346597534ce7f03dac91bdaa89560b229759ce6363a3afaf80a40486acba05fe701eeb06a6a77e35b43788fe8b129c3e91e0a03f8cf4ed6dc31dd7 + languageName: node + linkType: hard + "@react-navigation/elements@npm:^2.2.5": version: 2.2.5 resolution: "@react-navigation/elements@npm:2.2.5" @@ -5826,6 +5846,7 @@ __metadata: "@react-native/babel-preset": ^0.77.1 "@react-native/metro-config": ^0.77.1 "@react-native/typescript-config": ^0.77.1 + "@react-navigation/drawer": ^7.1.1 "@react-navigation/native": 7.0.14 "@react-navigation/native-stack": 7.2.0 "@reduxjs/toolkit": 2.2.6 @@ -12564,6 +12585,20 @@ __metadata: languageName: node linkType: hard +"react-native-drawer-layout@npm:^4.1.1": + version: 4.1.1 + resolution: "react-native-drawer-layout@npm:4.1.1" + dependencies: + use-latest-callback: ^0.2.1 + peerDependencies: + react: ">= 18.2.0" + react-native: "*" + react-native-gesture-handler: ">= 2.0.0" + react-native-reanimated: ">= 2.0.0" + checksum: 02366958e92f6d1ff6746cb1f85803e460235604ccf4970daebdc217f994b716d0334f361d1f087fac976d5e2beebfe8d17b6bc13e6236538d2dbd8d25226465 + languageName: node + linkType: hard + "react-native-fetch-api@npm:3.0.0": version: 3.0.0 resolution: "react-native-fetch-api@npm:3.0.0"