From 9283cc20698f97dc20b63e65cfc39032812251c1 Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Thu, 13 Mar 2025 02:01:51 +0100 Subject: [PATCH] refactor(ui): bottom sheets --- e2e/backup.e2e.js | 8 +- e2e/boost.e2e.js | 8 +- e2e/helpers.js | 4 +- e2e/lightning.e2e.js | 19 +- e2e/lnurl.e2e.js | 8 +- e2e/onchain.e2e.js | 17 +- e2e/receive.e2e.js | 3 +- e2e/security.e2e.js | 4 +- e2e/send.e2e.js | 4 +- e2e/settings.e2e.js | 1 + e2e/slashtags.e2e.js | 10 +- e2e/transfer.e2e.js | 8 +- e2e/widgets.e2e.js | 4 +- patches/@gorhom+bottom-sheet+4.6.4.patch | 30 +++ src/App.tsx | 5 +- src/AppOnboarded.tsx | 20 +- src/components/BottomSheet.tsx | 118 ++++++++++ src/components/BottomSheetWrapper.tsx | 201 ----------------- src/components/Dialog.tsx | 2 +- src/components/PinPad.tsx | 5 +- src/components/SlashtagsProvider.tsx | 3 +- src/components/Suggestions.tsx | 9 +- src/components/TabBar.tsx | 42 +--- src/hooks/bottomSheet.ts | 101 ++------- .../bottom-sheet/AppUpdatePrompt.tsx | 117 ---------- src/navigation/bottom-sheet/BackupPrompt.tsx | 116 ---------- src/navigation/bottom-sheet/BottomSheets.tsx | 38 ---- .../bottom-sheet/HighBalanceWarning.tsx | 139 ------------ .../bottom-sheet/LNURLWithdrawNavigation.tsx | 77 ------- src/navigation/bottom-sheet/PINNavigation.tsx | 62 ------ .../bottom-sheet/QuickPayPrompt.tsx | 118 ---------- .../bottom-sheet/ReceiveNavigation.tsx | 93 -------- .../bottom-sheet/SendNavigation.tsx | 189 ---------------- .../root/RootNavigationContainer.tsx | 16 +- src/navigation/root/RootNavigator.tsx | 9 +- src/navigation/types/index.ts | 21 +- src/screens/Activity/ActivityDetail.tsx | 12 +- src/screens/Activity/ActivityFiltered.tsx | 24 +- src/screens/Activity/ActivityListShort.tsx | 6 +- src/screens/Activity/ActivityTagsPrompt.tsx | 144 ------------ src/screens/Contacts/Contacts.tsx | 11 +- src/screens/OrangeTicket/Error.tsx | 7 +- src/screens/OrangeTicket/UsedCard.tsx | 7 +- src/screens/Profile/ProfileEdit.tsx | 9 +- src/screens/Profile/ProfileLink.tsx | 13 +- src/screens/Settings/AddressViewer/index.tsx | 13 +- src/screens/Settings/Backup/Metadata.tsx | 17 +- .../Settings/Backup/ResetAndRestore.tsx | 5 +- src/screens/Settings/Backup/ShowMnemonic.tsx | 3 - src/screens/Settings/BackupSettings/index.tsx | 6 +- src/screens/Settings/DevSettings/LdkDebug.tsx | 5 +- src/screens/Settings/PIN/AskForBiometrics.tsx | 3 - src/screens/Settings/PIN/ChangePin.tsx | 7 +- src/screens/Settings/PIN/PINPrompt.tsx | 14 +- src/screens/Settings/PIN/Result.tsx | 8 +- src/screens/Settings/Security/index.tsx | 4 +- src/screens/Transfer/Funding.tsx | 20 +- src/screens/TreasureHunt/Chest.tsx | 9 +- src/screens/Wallets/Home.tsx | 10 +- src/screens/Wallets/LNURLPay/Amount.tsx | 5 +- src/screens/Wallets/LNURLPay/Confirm.tsx | 15 +- src/screens/Wallets/LNURLWithdraw/Amount.tsx | 12 +- src/screens/Wallets/LNURLWithdraw/Confirm.tsx | 30 +-- src/screens/Wallets/NewTxPrompt.tsx | 172 -------------- src/screens/Wallets/Receive/ReceiveAmount.tsx | 3 - .../Wallets/Receive/ReceiveConnect.tsx | 2 +- .../Wallets/Receive/ReceiveGeoBlocked.tsx | 8 +- src/screens/Wallets/Receive/ReceiveQR.tsx | 54 +---- src/screens/Wallets/Send/Amount.tsx | 3 - src/screens/Wallets/Send/AutoRebalance.tsx | 7 +- src/screens/Wallets/Send/Error.tsx | 6 +- src/screens/Wallets/Send/Pending.tsx | 10 +- src/screens/Wallets/Send/Quickpay.tsx | 3 - src/screens/Wallets/Send/Recipient.tsx | 3 - src/screens/Wallets/Send/ReviewAndSend.tsx | 3 - src/screens/Wallets/Send/SendPinPad.tsx | 5 +- src/screens/Wallets/Send/Success.tsx | 11 +- src/sheets/ActivityTags.tsx | 144 ++++++++++++ .../Contacts => sheets}/AddContact.tsx | 57 ++--- src/sheets/AppUpdate.tsx | 97 ++++++++ .../BackupNavigation.tsx | 34 ++- src/sheets/BackupPrompt.tsx | 103 +++++++++ .../BoostPrompt.tsx => sheets/Boost.tsx} | 108 ++++----- .../BottomSheetNavigationContainer.tsx | 0 src/sheets/BottomSheets.tsx | 34 +++ .../BottomSheetsLazy.tsx | 0 .../ConnectionClosed.tsx | 30 +-- .../DatePicker.tsx} | 64 +++--- .../bottom-sheet => sheets}/ForceTransfer.tsx | 42 ++-- .../Settings/PIN => sheets}/ForgotPIN.tsx | 42 ++-- src/sheets/HighBalanceWarning.tsx | 124 +++++++++++ src/sheets/LNURLWithdrawNavigation.tsx | 68 ++++++ .../OrangeTicketNavigation.tsx | 100 ++++----- src/sheets/PINNavigation.tsx | 65 ++++++ .../ProfileLinkNavigation.tsx | 22 +- .../bottom-sheet => sheets}/PubkyAuth.tsx | 123 +++++----- src/sheets/QuickPayPrompt.tsx | 101 +++++++++ src/sheets/ReceiveNavigation.tsx | 98 ++++++++ src/sheets/ReceivedTransaction.tsx | 166 ++++++++++++++ src/sheets/SendNavigation.tsx | 210 ++++++++++++++++++ src/sheets/SheetRefsProvider.tsx | 47 ++++ .../TagsPrompt.tsx => sheets/Tags.tsx} | 36 +-- .../TransferFailed.tsx | 19 +- .../TreasureHuntNavigation.tsx | 110 ++++----- src/store/reselect/ui.ts | 33 +-- src/store/shapes/ui.ts | 32 +-- src/store/slices/ui.ts | 45 ---- src/store/types/ui.ts | 92 ++++---- src/store/utils/activity.ts | 11 +- src/store/utils/ui.ts | 48 ++-- src/utils/lightning/index.ts | 18 +- src/utils/scanner/scanner.ts | 154 ++++++++----- src/utils/slashtags/index.ts | 10 +- 113 files changed, 2224 insertions(+), 2601 deletions(-) create mode 100644 patches/@gorhom+bottom-sheet+4.6.4.patch create mode 100644 src/components/BottomSheet.tsx delete mode 100644 src/components/BottomSheetWrapper.tsx delete mode 100644 src/navigation/bottom-sheet/AppUpdatePrompt.tsx delete mode 100644 src/navigation/bottom-sheet/BackupPrompt.tsx delete mode 100644 src/navigation/bottom-sheet/BottomSheets.tsx delete mode 100644 src/navigation/bottom-sheet/HighBalanceWarning.tsx delete mode 100644 src/navigation/bottom-sheet/LNURLWithdrawNavigation.tsx delete mode 100644 src/navigation/bottom-sheet/PINNavigation.tsx delete mode 100644 src/navigation/bottom-sheet/QuickPayPrompt.tsx delete mode 100644 src/navigation/bottom-sheet/ReceiveNavigation.tsx delete mode 100644 src/navigation/bottom-sheet/SendNavigation.tsx delete mode 100644 src/screens/Activity/ActivityTagsPrompt.tsx delete mode 100644 src/screens/Wallets/NewTxPrompt.tsx create mode 100644 src/sheets/ActivityTags.tsx rename src/{screens/Contacts => sheets}/AddContact.tsx (69%) create mode 100644 src/sheets/AppUpdate.tsx rename src/{navigation/bottom-sheet => sheets}/BackupNavigation.tsx (60%) create mode 100644 src/sheets/BackupPrompt.tsx rename src/{screens/Wallets/BoostPrompt.tsx => sheets/Boost.tsx} (68%) rename src/{navigation/bottom-sheet => sheets}/BottomSheetNavigationContainer.tsx (100%) create mode 100644 src/sheets/BottomSheets.tsx rename src/{navigation/bottom-sheet => sheets}/BottomSheetsLazy.tsx (100%) rename src/{navigation/bottom-sheet => sheets}/ConnectionClosed.tsx (61%) rename src/{screens/Activity/TimeRangePrompt.tsx => sheets/DatePicker.tsx} (86%) rename src/{navigation/bottom-sheet => sheets}/ForceTransfer.tsx (72%) rename src/{screens/Settings/PIN => sheets}/ForgotPIN.tsx (53%) create mode 100644 src/sheets/HighBalanceWarning.tsx create mode 100644 src/sheets/LNURLWithdrawNavigation.tsx rename src/{navigation/bottom-sheet => sheets}/OrangeTicketNavigation.tsx (67%) create mode 100644 src/sheets/PINNavigation.tsx rename src/{navigation/bottom-sheet => sheets}/ProfileLinkNavigation.tsx (61%) rename src/{navigation/bottom-sheet => sheets}/PubkyAuth.tsx (64%) create mode 100644 src/sheets/QuickPayPrompt.tsx create mode 100644 src/sheets/ReceiveNavigation.tsx create mode 100644 src/sheets/ReceivedTransaction.tsx create mode 100644 src/sheets/SendNavigation.tsx create mode 100644 src/sheets/SheetRefsProvider.tsx rename src/{screens/Activity/TagsPrompt.tsx => sheets/Tags.tsx} (59%) rename src/{navigation/bottom-sheet => sheets}/TransferFailed.tsx (65%) rename src/{navigation/bottom-sheet => sheets}/TreasureHuntNavigation.tsx (57%) diff --git a/e2e/backup.e2e.js b/e2e/backup.e2e.js index e9af07f59..c584bdba9 100644 --- a/e2e/backup.e2e.js +++ b/e2e/backup.e2e.js @@ -66,10 +66,10 @@ d('Backup', () => { await rpc.generateToAddress(1, await rpc.getNewAddress()); await electrum?.waitForSync(); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(10000); - await element(by.id('NewTxPrompt')).swipe('down'); // close Receive screen + await element(by.id('ReceivedTransaction')).swipe('down'); // close Receive screen await sleep(200); // animation // set tag to new tx @@ -92,7 +92,7 @@ d('Backup', () => { await element(by.id('NavigationClose')).atIndex(0).tap(); // remove 2 default widgets, leave PriceWidget - await element(by.id('HomeScrollView')).scroll(200, 'down', 0); + await element(by.id('HomeScrollView')).scroll(200, 'down', 0, 0.5); await element(by.id('WidgetsEdit')).tap(); for (const w of ['NewsWidget', 'BlocksWidget']) { await element(by.id('WidgetActionDelete').withAncestor(by.id(w))).tap(); @@ -122,7 +122,7 @@ d('Backup', () => { await sleep(200); // animation // check widgets - await element(by.id('HomeScrollView')).scroll(300, 'down', 0); + await element(by.id('HomeScrollView')).scroll(300, 'down', 0, 0.5); await expect(element(by.id('PriceWidget'))).toExist(); await expect(element(by.id('NewsWidget'))).not.toExist(); await expect(element(by.id('BlocksWidget'))).not.toExist(); diff --git a/e2e/boost.e2e.js b/e2e/boost.e2e.js index 0b3374577..b22f38408 100644 --- a/e2e/boost.e2e.js +++ b/e2e/boost.e2e.js @@ -67,10 +67,10 @@ d('Boost', () => { await rpc.sendToAddress(wAddress, '0.001'); await rpc.generateToAddress(1, await rpc.getNewAddress()); await electrum?.waitForSync(); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(10000); - await element(by.id('NewTxPromptButton')).tap(); + await element(by.id('ReceivedTransactionButton')).tap(); await expect( element(by.id('MoneyText').withAncestor(by.id('TotalBalance'))), ).toHaveText('100 000'); @@ -178,10 +178,10 @@ d('Boost', () => { await rpc.sendToAddress(wAddress, '0.001'); await rpc.generateToAddress(1, await rpc.getNewAddress()); await electrum?.waitForSync(); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(10000); - await element(by.id('NewTxPromptButton')).tap(); + await element(by.id('ReceivedTransactionButton')).tap(); await expect( element(by.id('MoneyText').withAncestor(by.id('TotalBalance'))), ).toHaveText('100 000'); diff --git a/e2e/helpers.js b/e2e/helpers.js index dd0db1221..7929ad175 100644 --- a/e2e/helpers.js +++ b/e2e/helpers.js @@ -138,10 +138,10 @@ export const receiveOnchainFunds = async (rpc, amount = '0.001') => { await rpc.sendToAddress(wAddress, amount); await rpc.generateToAddress(1, await rpc.getNewAddress()); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(10000); - await element(by.id('NewTxPrompt')).swipe('down'); + await element(by.id('ReceivedTransaction')).swipe('down'); await sleep(1000); }; diff --git a/e2e/lightning.e2e.js b/e2e/lightning.e2e.js index 6bc9b5176..628244b16 100644 --- a/e2e/lightning.e2e.js +++ b/e2e/lightning.e2e.js @@ -131,10 +131,10 @@ d('Lightning', () => { let { label: invoice1 } = await element(by.id('QRCode')).getAttributes(); invoice1 = invoice1.replaceAll(/bitcoin.*=/gi, '').toLowerCase(); await lnd.sendPaymentSync({ paymentRequest: invoice1, amt: 50000 }); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(10000); - await element(by.id('NewTxPrompt')).swipe('down'); + await element(by.id('ReceivedTransaction')).swipe('down'); await waitFor( element(by.id('MoneyText').withAncestor(by.id('TotalBalance'))), ) @@ -153,20 +153,21 @@ d('Lightning', () => { const note1 = 'note 111'; await element(by.id('ReceiveNote')).typeText(note1); await element(by.id('ReceiveNote')).tapReturnKey(); - await sleep(200); + await sleep(300); // wait for keyboard to hide await element(by.id('TagsAdd')).tap(); await element(by.id('TagInputReceive')).typeText('rtag'); await element(by.id('TagInputReceive')).tapReturnKey(); + await sleep(300); // wait for keyboard to hide await element(by.id('ShowQrReceive')).tap(); await element(by.id('QRCode')).swipe('left'); const { label: invoice2 } = await element( by.id('ReceiveLightningInvoice'), ).getAttributes(); await lnd.sendPaymentSync({ paymentRequest: invoice2 }); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(10000); - await element(by.id('NewTxPrompt')).swipe('down'); + await element(by.id('ReceivedTransaction')).swipe('down'); await waitFor( element(by.id('MoneyText').withAncestor(by.id('TotalBalance'))), ) @@ -232,7 +233,7 @@ d('Lightning', () => { .withTimeout(10000); // check tx history - await element(by.id('HomeScrollView')).scroll(1000, 'down', 0); + await element(by.id('HomeScrollView')).scroll(1000, 'down', 0, 0.5); await expect( element(by.text('1 000').withAncestor(by.id('ActivityShort-1'))), ).toBeVisible(); @@ -324,7 +325,7 @@ d('Lightning', () => { .withTimeout(10000); // check tx history - await element(by.id('HomeScrollView')).scroll(1000, 'down', 0); + await element(by.id('HomeScrollView')).scroll(1000, 'down', 0, 0.5); await expect( element(by.text('111').withAncestor(by.id('ActivityShort-2'))), ).toBeVisible(); @@ -360,10 +361,10 @@ d('Lightning', () => { // TODO: for some reason this doen't work on github actions // wait for onchain payment to arrive - // await waitFor(element(by.id('NewTxPrompt'))) + // await waitFor(element(by.id('ReceivedTransaction'))) // .toBeVisible() // .withTimeout(60000); - // await element(by.id('NewTxPrompt')).swipe('down'); + // await element(by.id('ReceivedTransaction')).swipe('down'); // await expect( // element(by.id('MoneySign').withAncestor(by.id('ActivityShort-1'))), // ).toHaveText('+'); diff --git a/e2e/lnurl.e2e.js b/e2e/lnurl.e2e.js index 699f19cc4..ee4a63322 100644 --- a/e2e/lnurl.e2e.js +++ b/e2e/lnurl.e2e.js @@ -261,10 +261,10 @@ d('LNURL', () => { await element(by.id('DialogConfirm')).tap(); await element(by.id('ContinueAmount')).tap(); await element(by.id('WithdrawConfirmButton')).tap(); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(10000); - await element(by.id('NewTxPrompt')).swipe('down'); + await element(by.id('ReceivedTransaction')).swipe('down'); // test lnurl-withdraw, with min !== max amount const withdrawRequest2 = await lnurl.generateNewUrl('withdrawRequest', { @@ -277,10 +277,10 @@ d('LNURL', () => { await element(by.id('QRInput')).replaceText(withdrawRequest2.encoded); await element(by.id('DialogConfirm')).tap(); await element(by.id('WithdrawConfirmButton')).tap(); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(10000); - await element(by.id('NewTxPrompt')).swipe('down'); + await element(by.id('ReceivedTransaction')).swipe('down'); // test lnurl-auth const loginRequest1 = await lnurl.generateNewUrl('login'); diff --git a/e2e/onchain.e2e.js b/e2e/onchain.e2e.js index d739b868f..8c346a73d 100644 --- a/e2e/onchain.e2e.js +++ b/e2e/onchain.e2e.js @@ -67,16 +67,17 @@ d('Onchain', () => { await element(by.id('TagsAdd')).tap(); await element(by.id('TagInputReceive')).typeText(`rtag${i}`); await element(by.id('TagInputReceive')).tapReturnKey(); + await sleep(300); // wait for keyboard to hide await element(by.id('ShowQrReceive')).tap(); await rpc.sendToAddress(wAddress, '1'); await rpc.generateToAddress(1, await rpc.getNewAddress()); await electrum?.waitForSync(); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(10000); - await element(by.id('NewTxPrompt')).swipe('down'); // close Receive screen + await element(by.id('ReceivedTransaction')).swipe('down'); // close Receive screen await sleep(1000); // animation } @@ -140,7 +141,7 @@ d('Onchain', () => { ).toHaveText('0'); // check Activity - await element(by.id('HomeScrollView')).scroll(1000, 'down', 0); + await element(by.id('HomeScrollView')).scroll(1000, 'down', 0, 0.5); await expect(element(by.id('ActivityShort-1'))).toBeVisible(); await expect(element(by.id('ActivityShort-2'))).toBeVisible(); await expect(element(by.id('ActivityShort-3'))).toBeVisible(); @@ -200,7 +201,7 @@ d('Onchain', () => { await element(by.id('Tag-stag-delete')).tap(); // calendar, previous month, 0 transactions - await element(by.id('TimeRangePrompt')).tap(); + await element(by.id('DatePicker')).tap(); await expect(element(by.id('Today'))).toBeVisible(); await element(by.id('PrevMonth')).tap(); await expect(element(by.id('Today'))).not.toExist(); @@ -210,7 +211,7 @@ d('Onchain', () => { await expect(element(by.id('Activity-1'))).not.toExist(); // calendar, current date, 3 transactions - await element(by.id('TimeRangePrompt')).tap(); + await element(by.id('DatePicker')).tap(); await element(by.id('CalendarClearButton')).tap(); await element(by.id('NextMonth')).tap(); await element(by.id('Today')).tap(); @@ -237,10 +238,10 @@ d('Onchain', () => { // await rpc.generateToAddress(1, await rpc.getNewAddress()); // await electrum?.waitForSync(); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(10000); - await element(by.id('NewTxPrompt')).swipe('down'); // close Receive screen + await element(by.id('ReceivedTransaction')).swipe('down'); // close Receive screen await sleep(1000); // animation const coreAddress = await rpc.getNewAddress(); @@ -307,7 +308,7 @@ d('Onchain', () => { ).toHaveText('0'); // check number of outputs for send tx - await element(by.id('HomeScrollView')).scroll(1000, 'down', 0); + await element(by.id('HomeScrollView')).scroll(1000, 'down', 0, 0.5); await expect(element(by.id('ActivityShort-1'))).toBeVisible(); await expect(element(by.id('ActivityShort-2'))).toBeVisible(); await element(by.id('ActivityShowAll')).tap(); diff --git a/e2e/receive.e2e.js b/e2e/receive.e2e.js index f4cc6106e..54e031316 100644 --- a/e2e/receive.e2e.js +++ b/e2e/receive.e2e.js @@ -88,13 +88,14 @@ d('Receive', () => { const note = 'iPhone Refurbished'; await element(by.id('ReceiveNote')).typeText(note); await element(by.id('ReceiveNote')).tapReturnKey(); - await sleep(200); + await sleep(300); // wait for keyboard to hide // Tags const tag = 'test123'; await element(by.id('TagsAdd')).tap(); await element(by.id('TagInputReceive')).typeText(tag); await element(by.id('ReceiveTagsSubmit')).tap(); + await sleep(300); // wait for keyboard to hide // Show QR await element(by.id('ShowQrReceive')).tap(); diff --git a/e2e/security.e2e.js b/e2e/security.e2e.js index a96830b09..5d045a646 100644 --- a/e2e/security.e2e.js +++ b/e2e/security.e2e.js @@ -132,10 +132,10 @@ d('Settings Security And Privacy', () => { await rpc.sendToAddress(wAddress, '1'); await rpc.generateToAddress(1, await rpc.getNewAddress()); await electrum?.waitForSync(); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(10000); - await element(by.id('NewTxPrompt')).swipe('down'); // close Receive screen + await element(by.id('ReceivedTransaction')).swipe('down'); // close Receive screen // send, using FaceID const coreAddress = await rpc.getNewAddress(); diff --git a/e2e/send.e2e.js b/e2e/send.e2e.js index f7e6d54ef..56b33bdb2 100644 --- a/e2e/send.e2e.js +++ b/e2e/send.e2e.js @@ -245,10 +245,10 @@ d('Send', () => { let { label: receive } = await element(by.id('QRCode')).getAttributes(); receive = receive.replaceAll(/bitcoin.*=/gi, '').toLowerCase(); await lnd.sendPaymentSync({ paymentRequest: receive, amt: 50000 }); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(10000); - await element(by.id('NewTxPrompt')).swipe('down'); + await element(by.id('ReceivedTransaction')).swipe('down'); await waitFor( element(by.id('MoneyText').withAncestor(by.id('TotalBalance'))), diff --git a/e2e/settings.e2e.js b/e2e/settings.e2e.js index d99623c9e..01d5b03e8 100644 --- a/e2e/settings.e2e.js +++ b/e2e/settings.e2e.js @@ -193,6 +193,7 @@ d('Settings', () => { await expect(element(by.text(tag))).not.toBeVisible(); await element(by.id('TagInputReceive')).typeText(tag); await element(by.id('ReceiveTagsSubmit')).tap(); + await sleep(300); // wait for keyboard to hide await expect(element(by.text(tag))).toBeVisible(); await element(by.id('ReceiveScreen')).swipe('down'); await sleep(1000); diff --git a/e2e/slashtags.e2e.js b/e2e/slashtags.e2e.js index 5b9a739dd..4ff9a7222 100644 --- a/e2e/slashtags.e2e.js +++ b/e2e/slashtags.e2e.js @@ -143,12 +143,6 @@ d('Profile and Contacts', () => { await element(by.id('NavigationBack')).tap(); await element(by.id('NavigationBack')).tap(); - if (device.getPlatform() === 'ios') { - // FIXME: this bottom sheet should not appear - // Tap on background to dismiss - await element(by.label('Close')).atIndex(0).tap({ x: 10, y: 10 }); - } - // Hal await element(by.id('AddContact')).tap(); await element(by.id('ContactURLInput')).typeText(hal.url); @@ -206,10 +200,10 @@ d('Profile and Contacts', () => { await rpc.sendToAddress(wAddress, '1'); await rpc.generateToAddress(1, await rpc.getNewAddress()); await electrum?.waitForSync(); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(10000); - await element(by.id('NewTxPrompt')).swipe('down'); + await element(by.id('ReceivedTransaction')).swipe('down'); await element(by.id('ActivitySavings')).tap(); await element(by.id('Activity-1')).tap(); await element(by.id('ActivityAssign')).tap(); diff --git a/e2e/transfer.e2e.js b/e2e/transfer.e2e.js index df6f53706..7fb39cee4 100644 --- a/e2e/transfer.e2e.js +++ b/e2e/transfer.e2e.js @@ -75,10 +75,10 @@ d('Transfer', () => { await rpc.generateToAddress(1, await rpc.getNewAddress()); await electrum?.waitForSync(); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(20000); - await element(by.id('NewTxPrompt')).swipe('down'); // close Receive screen + await element(by.id('ReceivedTransaction')).swipe('down'); // close Receive screen // switch to USD await element(by.id('HeaderMenu')).tap(); @@ -261,10 +261,10 @@ d('Transfer', () => { await rpc.sendToAddress(wAddress, '0.001'); await rpc.generateToAddress(1, await rpc.getNewAddress()); - await waitFor(element(by.id('NewTxPrompt'))) + await waitFor(element(by.id('ReceivedTransaction'))) .toBeVisible() .withTimeout(20000); - await element(by.id('NewTxPrompt')).swipe('down'); + await element(by.id('ReceivedTransaction')).swipe('down'); // Get LDK node id await element(by.id('HeaderMenu')).tap(); diff --git a/e2e/widgets.e2e.js b/e2e/widgets.e2e.js index f8a74b8a8..210223f6b 100644 --- a/e2e/widgets.e2e.js +++ b/e2e/widgets.e2e.js @@ -35,7 +35,7 @@ d('Widgets', () => { } // add price widget - await element(by.id('HomeScrollView')).scroll(300, 'down', 0); + await element(by.id('HomeScrollView')).scroll(300, 'down', 0, 0.5); await element(by.id('WidgetsAdd')).tap(); await element(by.id('WidgetsOnboarding-button')).tap(); await element(by.id('WidgetListItem-price')).tap(); @@ -50,7 +50,7 @@ d('Widgets', () => { await element(by.id('WidgetEditField-showSource')).tap(); await element(by.id('WidgetEditPreview')).tap(); await element(by.id('WidgetSave')).tap(); - await element(by.id('HomeScrollView')).scroll(200, 'down', 0); + await element(by.id('HomeScrollView')).scroll(200, 'down', 0, 0.5); await expect(element(by.id('PriceWidget'))).toBeVisible(); await expect(element(by.id('PriceWidgetRow-BTC/EUR'))).toBeVisible(); await expect(element(by.id('PriceWidgetSource'))).toBeVisible(); diff --git a/patches/@gorhom+bottom-sheet+4.6.4.patch b/patches/@gorhom+bottom-sheet+4.6.4.patch new file mode 100644 index 000000000..3e5410805 --- /dev/null +++ b/patches/@gorhom+bottom-sheet+4.6.4.patch @@ -0,0 +1,30 @@ +diff --git a/node_modules/@gorhom/bottom-sheet/lib/typescript/types.d.ts b/node_modules/@gorhom/bottom-sheet/lib/typescript/types.d.ts +index 27f39a1..914efc6 100644 +--- a/node_modules/@gorhom/bottom-sheet/lib/typescript/types.d.ts ++++ b/node_modules/@gorhom/bottom-sheet/lib/typescript/types.d.ts +@@ -91,6 +91,10 @@ export interface BottomSheetModalMethods extends BottomSheetMethods { + * @see {WithTimingConfig} + */ + dismiss: (animationConfigs?: WithSpringConfig | WithTimingConfig) => void; ++ /** ++ * Check if the bottom sheet modal is open. ++ */ ++ isOpen: () => boolean; + } + //#endregion + +diff --git a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetModal/BottomSheetModal.tsx b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetModal/BottomSheetModal.tsx +index 275ce50..2d5ea19 100644 +--- a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetModal/BottomSheetModal.tsx ++++ b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetModal/BottomSheetModal.tsx +@@ -363,6 +363,10 @@ const BottomSheetModalComponent = forwardRef< + // internal + minimize: handleMinimize, + restore: handleRestore, ++ isOpen: () => { ++ // If animateOnMount is disabled, the sheet is open if it's mounted ++ return animateOnMount ? currentIndexRef.current !== -1 : mounted.current; ++ }, + })); + //#endregion + diff --git a/src/App.tsx b/src/App.tsx index 457d43465..f6b8c272f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,7 +15,6 @@ import { ThemeProvider } from 'styled-components/native'; import './utils/i18n'; import './utils/quick-actions'; import AppOnboarded from './AppOnboarded'; -import { SlashtagsProvider } from './components/SlashtagsProvider'; import { toastConfig } from './components/Toast'; import { useAppSelector } from './hooks/redux'; import AppUpdate from './screens/AppUpdate'; @@ -78,9 +77,7 @@ const App = (): ReactElement => { ) : hasCriticalUpdate ? ( ) : walletExists && !requiresRemoteRestore ? ( - - - + ) : ( diff --git a/src/AppOnboarded.tsx b/src/AppOnboarded.tsx index f91e4cd65..e0f51f961 100644 --- a/src/AppOnboarded.tsx +++ b/src/AppOnboarded.tsx @@ -1,10 +1,14 @@ +import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; import React, { memo, ReactElement } from 'react'; + import InactivityTracker from './components/InactivityTracker'; +import { SlashtagsProvider } from './components/SlashtagsProvider'; 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'; +import { SheetRefsProvider } from './sheets/SheetRefsProvider'; const AppOnboarded = (): ReactElement => { useWalletStartup(); @@ -12,11 +16,17 @@ const AppOnboarded = (): ReactElement => { useNetworkConnectivity(); return ( - - - - - + + + + + + + + + + + ); }; diff --git a/src/components/BottomSheet.tsx b/src/components/BottomSheet.tsx new file mode 100644 index 000000000..313140424 --- /dev/null +++ b/src/components/BottomSheet.tsx @@ -0,0 +1,118 @@ +import { + BottomSheetBackdrop, + BottomSheetBackdropProps, + BottomSheetBackgroundProps, + BottomSheetModal, + BottomSheetView, +} from '@gorhom/bottom-sheet'; +import React, { ReactNode, useCallback, useMemo } from 'react'; +import { StyleSheet } from 'react-native'; +import { useReducedMotion } from 'react-native-reanimated'; + +import { __E2E__ } from '../constants/env'; +import { useSnapPoints } from '../hooks/bottomSheet'; +import useColors from '../hooks/colors'; +import { useSheetRef } from '../sheets/SheetRefsProvider'; +import { SheetId } from '../store/types/ui'; +import BottomSheetBackground from './BottomSheetBackground'; + +type SheetProps = { + id: SheetId; + children: ((data: any) => ReactNode) | ReactNode; + size?: 'small' | 'medium' | 'large' | 'calendar'; + testID?: string; + onOpen?: () => void; + onClose?: () => void; +}; + +const Sheet = ({ + id, + children, + size = 'large', + testID, + onOpen, + onClose, +}: SheetProps) => { + const colors = useColors(); + const sheetRef = useSheetRef(id); + const isReducedMotion = useReducedMotion(); + const snapPoints = useSnapPoints(size); + + // https://github.com/gorhom/react-native-bottom-sheet/issues/770#issuecomment-1072113936 + // do not activate BottomSheet if swipe horizontally, this allows using Swiper inside of it + const activeOffsetX = useMemo(() => [-999, 999], []); + const activeOffsetY = useMemo(() => [-10, 10], []); + + const backdropComponent = useCallback((props: BottomSheetBackdropProps) => { + return ( + + ); + }, []); + + const backgroundComponent = useCallback( + ({ style }: BottomSheetBackgroundProps) => ( + + ), + [], + ); + + const onChange = useCallback( + (index: number) => { + if (index === -1) { + onClose?.(); + } else if (index >= 0) { + onOpen?.(); + } + }, + [onOpen, onClose], + ); + + return ( + + {(data) => { + return ( + + {typeof children === 'function' ? children(data) : children} + + ); + }} + + ); +}; + +const styles = StyleSheet.create({ + container: { + borderTopLeftRadius: 32, + borderTopRightRadius: 32, + height: '100%', + position: 'relative', + }, + handle: { + alignSelf: 'center', + height: 32, + width: 32, + }, +}); + +export default Sheet; diff --git a/src/components/BottomSheetWrapper.tsx b/src/components/BottomSheetWrapper.tsx deleted file mode 100644 index 6e25f9aee..000000000 --- a/src/components/BottomSheetWrapper.tsx +++ /dev/null @@ -1,201 +0,0 @@ -/*********************************************************************************** - * This component wraps the @gorhom/bottom-sheet library - * to more easily take advantage of it throughout the app. - * - * Implementation: - * const snapPoints = useSnapPoints('medium'); - * - * - * ... - * - * - * Usage Throughout App: - * dispatch(showBottomSheet('viewName')); - * dispatch(showBottomSheet('viewName', { option1: 'value' })); - * dispatch(closeSheet('viewName')); - * - * Check if a given view is open: - * getStore().user.viewController['viewName'].isOpen; - ***********************************************************************************/ - -import BottomSheet, { - BottomSheetView, - BottomSheetBackdrop, - BottomSheetBackgroundProps, - BottomSheetBackdropProps, -} from '@gorhom/bottom-sheet'; -import React, { - memo, - ReactElement, - forwardRef, - useImperativeHandle, - useRef, - useEffect, - useCallback, - useMemo, - useState, -} from 'react'; -import { StyleSheet } from 'react-native'; -import { useReducedMotion } from 'react-native-reanimated'; -import { useTheme } from 'styled-components/native'; - -import { __E2E__ } from '../constants/env'; -import { useAppDispatch, useAppSelector } from '../hooks/redux'; -import { viewControllerSelector } from '../store/reselect/ui'; -import { closeSheet } from '../store/slices/ui'; -import { TViewController } from '../store/types/ui'; -import BottomSheetBackground from './BottomSheetBackground'; - -export interface BottomSheetWrapperProps { - children: ReactElement; - view: TViewController; - snapPoints: number[]; - backdrop?: boolean; - testID?: string; - onOpen?: () => void; - onClose?: () => void; -} - -const BottomSheetWrapper = forwardRef( - ( - { - children, - view, - snapPoints, - backdrop = true, - testID, - onOpen, - onClose, - }: BottomSheetWrapperProps, - ref, - ): ReactElement => { - const bottomSheetRef = useRef(null); - const reducedMotion = useReducedMotion(); - const dispatch = useAppDispatch(); - const data = useAppSelector((state) => viewControllerSelector(state, view)); - const theme = useTheme(); - const handleIndicatorStyle = useMemo( - () => ({ backgroundColor: theme.colors.gray2 }), - [theme.colors.gray2], - ); - const [mounted, setMounted] = useState(false); - - // https://github.com/gorhom/react-native-bottom-sheet/issues/770#issuecomment-1072113936 - // do not activate BottomSheet if swipe horizontally, this allows using Swiper inside of it - const activeOffsetX = useMemo(() => [-999, 999], []); - const activeOffsetY = useMemo(() => [-10, 10], []); - - useEffect(() => { - if (data.isOpen) { - bottomSheetRef.current?.snapToIndex(0); - } else { - bottomSheetRef.current?.close(); - } - setTimeout(() => setMounted(true), 500); - }, [data.isOpen]); - - useImperativeHandle(ref, () => ({ - snapToIndex(index = 0): void { - bottomSheetRef.current?.snapToIndex(index); - }, - expand(): void { - bottomSheetRef.current?.snapToIndex(1); - }, - close(): void { - bottomSheetRef.current?.close(); - }, - })); - - const _onOpen = useCallback(() => onOpen?.(), [onOpen]); - - const _onClose = useCallback(() => { - if (data.isOpen) { - dispatch(closeSheet(view)); - } - onClose?.(); - }, [data.isOpen, view, onClose, dispatch]); - - // callbacks - const handleSheetChanges = useCallback( - (index: number) => { - if (index === -1) { - _onClose(); - } else if (index >= 0) { - _onOpen(); - } - }, - [_onClose, _onOpen], - ); - - const renderBackdrop = useCallback( - (props: BottomSheetBackdropProps) => { - if (!backdrop) { - return null; - } - return ( - - ); - }, - [backdrop], - ); - - const backgroundComponent = useCallback( - ({ style }: BottomSheetBackgroundProps) => ( - - ), - [], - ); - - const style = useMemo( - () => [styles.container, !mounted && { minHeight: snapPoints[0] - 30 }], - [snapPoints, mounted], - ); - - // Determine initial snapPoint index based on provided data. - const index = useMemo((): number => (data.isOpen ? 0 : -1), [data.isOpen]); - - return ( - - - {children} - - - ); - }, -); - -const styles = StyleSheet.create({ - container: { - borderTopLeftRadius: 32, - borderTopRightRadius: 32, - height: '100%', - position: 'relative', - }, - handle: { - alignSelf: 'center', - height: 32, - width: 32, - }, -}); - -export default memo(BottomSheetWrapper); diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx index aca3ce4d3..5b74bbf4f 100644 --- a/src/components/Dialog.tsx +++ b/src/components/Dialog.tsx @@ -86,7 +86,7 @@ const Dialog = ({ activeOpacity={0.7} testID="DialogConfirm" onPress={onConfirm}> - {confirmText} + {confirmText} )} diff --git a/src/components/PinPad.tsx b/src/components/PinPad.tsx index 903535a1a..903419722 100644 --- a/src/components/PinPad.tsx +++ b/src/components/PinPad.tsx @@ -6,7 +6,7 @@ import { FadeIn, FadeOut } from 'react-native-reanimated'; import BitkitLogo from '../assets/bitkit-logo.svg'; import { PIN_ATTEMPTS } from '../constants/app'; import usePIN from '../hooks/pin'; -import { showBottomSheet } from '../store/utils/ui'; +import { useSheetRef } from '../sheets/SheetRefsProvider'; import { AnimatedView, View as ThemedView } from '../styles/components'; import { FaceIdIcon, TouchIdIcon } from '../styles/icons'; import { BodyS, Subtitle } from '../styles/text'; @@ -31,6 +31,7 @@ const PinPad = ({ onShowBiotmetrics?: () => void; }): ReactElement => { const { t } = useTranslation('security'); + const sheetRef = useSheetRef('forgotPin'); const [biometryData, setBiometricData] = useState(); const { attemptsRemaining, Dots, handleNumberPress, isLastAttempt, loading } = usePIN(onSuccess); @@ -89,7 +90,7 @@ const PinPad = ({ ) : ( { - showBottomSheet('forgotPIN'); + sheetRef.current?.present(); }}> {t('pin_attempts', { attemptsRemaining })} diff --git a/src/components/SlashtagsProvider.tsx b/src/components/SlashtagsProvider.tsx index 02963675c..5894dc204 100644 --- a/src/components/SlashtagsProvider.tsx +++ b/src/components/SlashtagsProvider.tsx @@ -3,8 +3,7 @@ import SlashtagsProfile from '@synonymdev/slashtags-profile'; import { format, parse } from '@synonymdev/slashtags-url'; import type { Client as IWebRelayClient } from '@synonymdev/web-relay'; import { Client, Store } from '@synonymdev/web-relay/lib/client'; -import React, { ReactElement, useEffect, useState } from 'react'; -import { createContext } from 'react'; +import React, { ReactElement, createContext, useEffect, useState } from 'react'; import { useAppSelector } from '../hooks/redux'; import { WebRelayCache } from '../storage'; diff --git a/src/components/Suggestions.tsx b/src/components/Suggestions.tsx index b827af91a..eceb2c200 100644 --- a/src/components/Suggestions.tsx +++ b/src/components/Suggestions.tsx @@ -7,6 +7,7 @@ import Carousel from 'react-native-reanimated-carousel'; import { appName, appStoreUrl, playStoreUrl } from '../constants/app'; import { useAppDispatch, useAppSelector } from '../hooks/redux'; import type { RootNavigationProp } from '../navigation/types'; +import { useSheetRef } from '../sheets/SheetRefsProvider'; import { pinSelector, quickpayIntroSeenSelector, @@ -18,7 +19,6 @@ import { } from '../store/reselect/todos'; import { channelsNotificationsShown, hideTodo } from '../store/slices/todos'; import { ITodo, TTodoType } from '../store/types/todos'; -import { showBottomSheet } from '../store/utils/ui'; import { View as ThemedView } from '../styles/components'; import { Caption13Up } from '../styles/text'; import { getDurationForBlocks } from '../utils/helpers'; @@ -27,6 +27,8 @@ import SuggestionCard from './SuggestionCard'; const Suggestions = (): ReactElement => { const { t } = useTranslation('cards'); const navigation = useNavigation(); + const backupSheetRef = useSheetRef('backupPrompt'); + const pinSheetRef = useSheetRef('pinNavigation'); const { width } = useWindowDimensions(); const dispatch = useAppDispatch(); const pinTodoDone = useAppSelector(pinSelector); @@ -61,10 +63,11 @@ const Suggestions = (): ReactElement => { }); }, [t]); + // biome-ignore lint/correctness/useExhaustiveDependencies: sheetRef doesn't change const handleOnPress = useCallback( (id: TTodoType): void => { if (id === 'backupSeedPhrase') { - showBottomSheet('backupPrompt'); + backupSheetRef.current?.present(); } if (id === 'lightning') { @@ -81,7 +84,7 @@ const Suggestions = (): ReactElement => { if (id === 'pin') { if (!pinTodoDone) { - showBottomSheet('PINNavigation', { showLaterButton: true }); + pinSheetRef.current?.present(); } else { navigation.navigate('Settings', { screen: 'DisablePin' }); } diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx index 488fcbd01..157dd7230 100644 --- a/src/components/TabBar.tsx +++ b/src/components/TabBar.tsx @@ -1,14 +1,14 @@ import { useNavigation } from '@react-navigation/native'; -import React, { ReactElement, useMemo } from 'react'; +import React, { ReactElement, useMemo, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Platform, Pressable, StyleProp, StyleSheet, + View, ViewStyle, } from 'react-native'; -import Animated, { FadeIn } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { receiveIcon, sendIcon } from '../assets/icons'; @@ -16,11 +16,10 @@ import useColors from '../hooks/colors'; import { useAppSelector } from '../hooks/redux'; import { rootNavigation } from '../navigation/root/RootNavigationContainer'; import type { RootNavigationProp } from '../navigation/types'; +import { useSheetRef } from '../sheets/SheetRefsProvider'; import { resetSendTransaction } from '../store/actions/wallet'; import { spendingOnboardingSelector } from '../store/reselect/aggregations'; -import { viewControllersSelector } from '../store/reselect/ui'; -import { TViewController } from '../store/types/ui'; -import { toggleBottomSheet } from '../store/utils/ui'; +import { showSheet } from '../store/utils/ui'; import { ScanIcon } from '../styles/icons'; import ButtonBlur from './buttons/ButtonBlur'; @@ -29,38 +28,25 @@ const TabBar = (): ReactElement => { const insets = useSafeAreaInsets(); const { t } = useTranslation('wallet'); const navigation = useNavigation(); - const viewControllers = useAppSelector(viewControllersSelector); + const sendSheetRef = useSheetRef('send'); + const receiveSheetRef = useSheetRef('receive'); const isSpendingOnboarding = useAppSelector(spendingOnboardingSelector); - const shouldHide = useMemo(() => { - const viewControllerKeys: TViewController[] = [ - 'backupPrompt', - 'PINNavigation', - 'highBalance', - 'appUpdatePrompt', - 'timeRangePrompt', - 'tagsPrompt', - ]; - return viewControllerKeys.some((view) => viewControllers[view].isOpen); - }, [viewControllers]); - const onReceivePress = (): void => { const currentRoute = rootNavigation.getCurrentRoute(); // if we are on the spending screen and the user has not yet received funds if (currentRoute === 'ActivitySpending' && isSpendingOnboarding) { - toggleBottomSheet('receiveNavigation', { - receiveScreen: 'ReceiveAmount', - }); + showSheet('receive', { screen: 'ReceiveAmount' }); } else { - toggleBottomSheet('receiveNavigation'); + receiveSheetRef.current?.present(); } }; const onSendPress = (): void => { // make sure we start with a clean transaction state resetSendTransaction(); - toggleBottomSheet('sendNavigation'); + sendSheetRef.current?.present(); }; const onScanPress = (): void => navigation.navigate('Scanner'); @@ -82,12 +68,8 @@ const TabBar = (): ReactElement => { const sendXml = sendIcon('white'); const receiveXml = receiveIcon('white'); - if (shouldHide) { - return <>; - } - return ( - + { testID="Receive" onPress={onReceivePress} /> - + ); }; @@ -149,4 +131,4 @@ const styles = StyleSheet.create({ }, }); -export default TabBar; +export default memo(TabBar); diff --git a/src/hooks/bottomSheet.ts b/src/hooks/bottomSheet.ts index 6b73e55b7..70c71d6c2 100644 --- a/src/hooks/bottomSheet.ts +++ b/src/hooks/bottomSheet.ts @@ -1,19 +1,11 @@ -import { useFocusEffect, useNavigation } from '@react-navigation/native'; -import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { BackHandler, NativeEventSubscription } from 'react-native'; +import { useEffect, useMemo } from 'react'; +import { BackHandler } from 'react-native'; import { useSafeAreaFrame, useSafeAreaInsets, } from 'react-native-safe-area-context'; -import { - viewControllerIsOpenSelector, - viewControllersSelector, -} from '../store/reselect/ui'; -import { closeAllSheets, closeSheet } from '../store/slices/ui'; -import { TViewController } from '../store/types/ui'; -import { objectKeys } from '../utils/objectKeys'; -import { useAppDispatch, useAppSelector } from './redux'; +import { useAllSheetRefs } from '../sheets/SheetRefsProvider'; export const useSnapPoints = ( size: 'small' | 'medium' | 'large' | 'calendar', @@ -50,81 +42,32 @@ export const useSnapPoints = ( }; /** - * Hook to handle hardware back press (Android) when bottom sheet is open - * for simple one-sheet screens + * Hook to handle hardware back press (Android) when a bottom sheet is open */ -export const useBottomSheetBackPress = ( - viewController: TViewController, -): void => { - const dispatch = useAppDispatch(); - const isBottomSheetOpen = useAppSelector((state) => { - return viewControllerIsOpenSelector(state, viewController); - }); - - const backHandlerSubscriptionRef = useRef( - null, - ); +export const useBottomSheetBackPress = (): void => { + const sheetRefs = useAllSheetRefs(); + // biome-ignore lint/correctness/useExhaustiveDependencies: sheetRefs don't change useEffect(() => { - if (!isBottomSheetOpen) { - return; - } + const backAction = () => { + const openSheets = sheetRefs.filter(({ ref }) => { + return ref.current?.isOpen(); + }); - backHandlerSubscriptionRef.current = BackHandler.addEventListener( - 'hardwareBackPress', - () => { - dispatch(closeSheet(viewController)); + if (openSheets.length !== 0) { + openSheets.forEach(({ ref }) => ref.current?.close()); return true; - }, - ); + } - return (): void => { - backHandlerSubscriptionRef.current?.remove(); - backHandlerSubscriptionRef.current = null; + // if no sheets are open, let the event to bubble up + return false; }; - }, [isBottomSheetOpen, viewController, dispatch]); -}; - -/** - * Hook to handle hardware back press (Android) when bottom sheet is open - * for screens that are part of a navigator nested in a bottom sheet - */ -export const useBottomSheetScreenBackPress = (): void => { - const dispatch = useAppDispatch(); - const navigation = useNavigation(); - const viewControllers = useAppSelector(viewControllersSelector); - - const isBottomSheetOpen = useMemo(() => { - const viewControllerKeys = objectKeys(viewControllers); - return viewControllerKeys.some((view) => viewControllers[view].isOpen); - }, [viewControllers]); - - const backHandlerSubscriptionRef = useRef( - null, - ); - useFocusEffect( - useCallback(() => { - if (!isBottomSheetOpen) { - return; - } - - backHandlerSubscriptionRef.current = BackHandler.addEventListener( - 'hardwareBackPress', - () => { - if (navigation.canGoBack()) { - navigation.goBack(); - } else { - dispatch(closeAllSheets()); - } - return true; - }, - ); + const backHandler = BackHandler.addEventListener( + 'hardwareBackPress', + backAction, + ); - return (): void => { - backHandlerSubscriptionRef.current?.remove(); - backHandlerSubscriptionRef.current = null; - }; - }, [dispatch, isBottomSheetOpen, navigation]), - ); + return () => backHandler.remove(); + }, []); }; diff --git a/src/navigation/bottom-sheet/AppUpdatePrompt.tsx b/src/navigation/bottom-sheet/AppUpdatePrompt.tsx deleted file mode 100644 index 19e845302..000000000 --- a/src/navigation/bottom-sheet/AppUpdatePrompt.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React, { memo, ReactElement, useEffect, useMemo } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; - -import BottomSheetScreen from '../../components/BottomSheetScreen'; -import BottomSheetWrapper from '../../components/BottomSheetWrapper'; -import { __E2E__ } from '../../constants/env'; -import { - useBottomSheetBackPress, - useSnapPoints, -} from '../../hooks/bottomSheet'; -import { useAppDispatch, useAppSelector } from '../../hooks/redux'; -import { - availableUpdateSelector, - viewControllersSelector, -} from '../../store/reselect/ui'; -import { ignoreAppUpdateTimestampSelector } from '../../store/reselect/user'; -import { closeSheet } from '../../store/slices/ui'; -import { ignoreAppUpdate } from '../../store/slices/user'; -import { showBottomSheet } from '../../store/utils/ui'; -import { Display } from '../../styles/text'; -import { openURL } from '../../utils/helpers'; -import { objectKeys } from '../../utils/objectKeys'; - -const imageSrc = require('../../assets/illustrations/wand.png'); - -const ASK_INTERVAL = 1000 * 60 * 60 * 12; // 12h - how long this prompt will be hidden if user taps Later -const CHECK_DELAY = 2500; // how long user needs to stay on Wallets screen before he will see this prompt - -const AppUpdatePrompt = (): ReactElement => { - const { t } = useTranslation('other'); - const snapPoints = useSnapPoints('large'); - const dispatch = useAppDispatch(); - const viewControllers = useAppSelector(viewControllersSelector); - const updateInfo = useAppSelector(availableUpdateSelector); - const ignoreTimestamp = useAppSelector(ignoreAppUpdateTimestampSelector); - - useBottomSheetBackPress('appUpdatePrompt'); - - const anyBottomSheetIsOpen = useMemo(() => { - const viewControllerKeys = objectKeys(viewControllers); - return viewControllerKeys - .filter((view) => view !== 'appUpdatePrompt') - .some((view) => viewControllers[view].isOpen); - }, [viewControllers]); - - // if optional app update available - // and user has not seen this prompt for ASK_INTERVAL - // and no other bottom-sheets are shown - // and user on "Wallets" screen for CHECK_DELAY - const shouldShowBottomSheet = useMemo(() => { - const isTimeoutOver = Number(new Date()) - ignoreTimestamp > ASK_INTERVAL; - return ( - !__E2E__ && - updateInfo !== null && - !updateInfo.critical && - isTimeoutOver && - !anyBottomSheetIsOpen - ); - }, [updateInfo, ignoreTimestamp, anyBottomSheetIsOpen]); - - useEffect(() => { - if (!shouldShowBottomSheet) { - return; - } - - const timer = setTimeout(() => { - showBottomSheet('appUpdatePrompt'); - }, CHECK_DELAY); - - return (): void => { - clearTimeout(timer); - }; - }, [shouldShowBottomSheet]); - - const onClose = (): void => { - dispatch(ignoreAppUpdate()); - }; - - const onCancel = (): void => { - dispatch(ignoreAppUpdate()); - dispatch(closeSheet('appUpdatePrompt')); - }; - - const onUpdate = async (): Promise => { - dispatch(ignoreAppUpdate()); - await openURL(updateInfo?.url!); - dispatch(closeSheet('appUpdatePrompt')); - }; - - return ( - - }} - /> - } - description={t('update_text')} - image={imageSrc} - showBackButton={false} - continueText={t('update_button')} - cancelText={t('cancel')} - testID="AppUpdatePrompt" - onContinue={onUpdate} - onCancel={onCancel} - /> - - ); -}; - -export default memo(AppUpdatePrompt); diff --git a/src/navigation/bottom-sheet/BackupPrompt.tsx b/src/navigation/bottom-sheet/BackupPrompt.tsx deleted file mode 100644 index ccbd6fa1b..000000000 --- a/src/navigation/bottom-sheet/BackupPrompt.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React, { memo, ReactElement, useMemo, useEffect } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; - -import BottomSheetScreen from '../../components/BottomSheetScreen'; -import BottomSheetWrapper from '../../components/BottomSheetWrapper'; -import { __E2E__ } from '../../constants/env'; -import { - useBottomSheetBackPress, - useSnapPoints, -} from '../../hooks/bottomSheet'; -import { useAppDispatch, useAppSelector } from '../../hooks/redux'; -import { useBalance } from '../../hooks/wallet'; -import { viewControllersSelector } from '../../store/reselect/ui'; -import { backupVerifiedSelector } from '../../store/reselect/user'; -import { ignoreBackupTimestampSelector } from '../../store/reselect/user'; -import { closeSheet } from '../../store/slices/ui'; -import { ignoreBackup } from '../../store/slices/user'; -import { showBottomSheet } from '../../store/utils/ui'; -import { Display } from '../../styles/text'; -import { objectKeys } from '../../utils/objectKeys'; - -const imageSrc = require('../../assets/illustrations/safe.png'); - -const ASK_INTERVAL = 1000 * 60 * 60 * 24; // 1 day - how long this prompt will be hidden if user taps Later -const CHECK_DELAY = 2000; // how long user needs to stay on Wallets screen before he will see this prompt - -const BackupPrompt = (): ReactElement => { - const { t } = useTranslation('security'); - const snapPoints = useSnapPoints('medium'); - const dispatch = useAppDispatch(); - const viewControllers = useAppSelector(viewControllersSelector); - const ignoreTimestamp = useAppSelector(ignoreBackupTimestampSelector); - const backupVerified = useAppSelector(backupVerifiedSelector); - const { totalBalance } = useBalance(); - - useBottomSheetBackPress('backupPrompt'); - - const anyBottomSheetIsOpen = useMemo(() => { - const viewControllerKeys = objectKeys(viewControllers); - return viewControllerKeys - .filter((view) => view !== 'backupPrompt') - .some((view) => viewControllers[view].isOpen); - }, [viewControllers]); - - const handleLater = (): void => { - dispatch(ignoreBackup()); - dispatch(closeSheet('backupPrompt')); - }; - - const handleBackup = (): void => { - dispatch(closeSheet('backupPrompt')); - showBottomSheet('backupNavigation'); - }; - - // if backup has not been verified - // and wallet has transactions - // and user has not seen this prompt for ASK_INTERVAL - // and no other bottom-sheets are shown - // and user on "Wallets" screen for CHECK_DELAY - const shouldShowBottomSheet = useMemo(() => { - const isTimeoutOver = Number(new Date()) - ignoreTimestamp > ASK_INTERVAL; - return ( - !__E2E__ && - !backupVerified && - totalBalance > 0 && - isTimeoutOver && - !anyBottomSheetIsOpen - ); - }, [backupVerified, totalBalance, ignoreTimestamp, anyBottomSheetIsOpen]); - - useEffect(() => { - if (!shouldShowBottomSheet) { - return; - } - - const timer = setTimeout(() => { - showBottomSheet('backupPrompt'); - }, CHECK_DELAY); - - return (): void => { - clearTimeout(timer); - }; - }, [shouldShowBottomSheet]); - - const text = totalBalance > 0 ? t('backup_funds') : t('backup_funds_no'); - - return ( - { - dispatch(ignoreBackup()); - }}> - }} - /> - } - description={text} - image={imageSrc} - showBackButton={false} - continueText={t('backup_button')} - cancelText={t('later')} - testID="BackupPrompt" - onContinue={handleBackup} - onCancel={handleLater} - /> - - ); -}; - -export default memo(BackupPrompt); diff --git a/src/navigation/bottom-sheet/BottomSheets.tsx b/src/navigation/bottom-sheet/BottomSheets.tsx deleted file mode 100644 index 782d1ed06..000000000 --- a/src/navigation/bottom-sheet/BottomSheets.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { JSX, memo } from 'react'; -import { useAppSelector } from '../../hooks/redux'; -import { viewControllersSelector } from '../../store/reselect/ui'; -// import TransferFailed from '../bottom-sheet/TransferFailed'; - -import BoostPrompt from '../../screens/Wallets/BoostPrompt'; -import NewTxPrompt from '../../screens/Wallets/NewTxPrompt'; -import BackupNavigation from './BackupNavigation'; -import ConnectionClosed from './ConnectionClosed'; -import LNURLWithdrawNavigation from './LNURLWithdrawNavigation'; -import OrangeTicketNavigation from './OrangeTicketNavigation'; -import PINNavigation from './PINNavigation'; -// import TreasureHuntNavigation from './TreasureHuntNavigation'; -import PubkyAuth from './PubkyAuth'; -import ReceiveNavigation from './ReceiveNavigation'; -import SendNavigation from './SendNavigation'; - -const BottomSheets = (): JSX.Element => { - const views = useAppSelector(viewControllersSelector); - - return ( - <> - {views.backupNavigation.isMounted && } - {views.boostPrompt.isMounted && } - {views.connectionClosed.isMounted && } - {views.lnurlWithdraw.isMounted && } - {views.newTxPrompt.isMounted && } - {views.orangeTicket.isMounted && } - {views.PINNavigation.isMounted && } - {views.receiveNavigation.isMounted && } - {views.sendNavigation.isMounted && } - {/* {views.treasureHunt.isMounted && } */} - {views.pubkyAuth.isMounted && } - - ); -}; - -export default memo(BottomSheets); diff --git a/src/navigation/bottom-sheet/HighBalanceWarning.tsx b/src/navigation/bottom-sheet/HighBalanceWarning.tsx deleted file mode 100644 index 63fc0fa2a..000000000 --- a/src/navigation/bottom-sheet/HighBalanceWarning.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import React, { memo, ReactElement, useEffect, useMemo } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; - -import BottomSheetScreen from '../../components/BottomSheetScreen'; -import BottomSheetWrapper from '../../components/BottomSheetWrapper'; -import { __E2E__ } from '../../constants/env'; -import { - useBottomSheetBackPress, - useSnapPoints, -} from '../../hooks/bottomSheet'; -import { useAppDispatch, useAppSelector } from '../../hooks/redux'; -import { useBalance } from '../../hooks/wallet'; -import { viewControllersSelector } from '../../store/reselect/ui'; -import { - ignoreHighBalanceCountSelector, - ignoreHighBalanceTimestampSelector, -} from '../../store/reselect/user'; -import { exchangeRatesSelector } from '../../store/reselect/wallet'; -import { closeSheet } from '../../store/slices/ui'; -import { MAX_WARNINGS, ignoreHighBalance } from '../../store/slices/user'; -import { showBottomSheet } from '../../store/utils/ui'; -import { BodyMB, Display } from '../../styles/text'; -import { getFiatDisplayValues } from '../../utils/displayValues'; -import { openURL } from '../../utils/helpers'; -import { objectKeys } from '../../utils/objectKeys'; - -const imageSrc = require('../../assets/illustrations/exclamation-mark.png'); - -const BALANCE_THRESHOLD_USD = 500; // how high the balance must be to show this warning to the user (in USD) -const BALANCE_THRESHOLD_SATS = 700000; // how high the balance must be to show this warning to the user (in Sats) -const ASK_INTERVAL = 1000 * 60 * 60 * 24; // 1 day - how long this prompt will be hidden if user taps Later -const CHECK_DELAY = 3000; // how long user needs to stay on Wallets screen before he will see this prompt - -const HighBalanceWarning = (): ReactElement => { - const { t } = useTranslation('other'); - const { totalBalance } = useBalance(); - const snapPoints = useSnapPoints('large'); - const dispatch = useAppDispatch(); - const count = useAppSelector(ignoreHighBalanceCountSelector); - const exchangeRates = useAppSelector(exchangeRatesSelector); - const viewControllers = useAppSelector(viewControllersSelector); - const ignoreTimestamp = useAppSelector(ignoreHighBalanceTimestampSelector); - - useBottomSheetBackPress('highBalance'); - - const anyBottomSheetIsOpen = useMemo(() => { - const viewControllerKeys = objectKeys(viewControllers); - return viewControllerKeys - .filter((view) => view !== 'highBalance') - .some((view) => viewControllers[view].isOpen); - }, [viewControllers]); - - const { fiatValue } = getFiatDisplayValues({ - satoshis: totalBalance, - currency: 'USD', - exchangeRates, - }); - - // if balance over BALANCE_THRESHOLD - // and not more than MAX_WARNINGS times - // and user has not seen this prompt for ASK_INTERVAL - // and no other bottom-sheets are shown - // and user on "Wallets" screen for CHECK_DELAY - const shouldShowBottomSheet = useMemo(() => { - const thresholdReached = - // fallback in case exchange rates are not available - fiatValue !== 0 - ? fiatValue > BALANCE_THRESHOLD_USD - : totalBalance > BALANCE_THRESHOLD_SATS; - const belowMaxWarnings = count < MAX_WARNINGS; - const isTimeoutOver = Number(new Date()) - ignoreTimestamp > ASK_INTERVAL; - return ( - !__E2E__ && - thresholdReached && - belowMaxWarnings && - isTimeoutOver && - !anyBottomSheetIsOpen - ); - }, [fiatValue, totalBalance, count, ignoreTimestamp, anyBottomSheetIsOpen]); - - useEffect(() => { - if (!shouldShowBottomSheet) { - return; - } - - const timer = setTimeout(() => { - showBottomSheet('highBalance'); - }, CHECK_DELAY); - - return (): void => { - clearTimeout(timer); - }; - }, [shouldShowBottomSheet]); - - const onMore = (): void => { - openURL('https://en.bitcoin.it/wiki/Storing_bitcoins'); - }; - - const onDismiss = (): void => { - dispatch(ignoreHighBalance(true)); - dispatch(closeSheet('highBalance')); - }; - - return ( - { - dispatch(ignoreHighBalance(false)); - }}> - }} - /> - } - description={ - }} - /> - } - image={imageSrc} - showBackButton={false} - continueText={t('high_balance.continue')} - cancelText={t('high_balance.cancel')} - testID="HighBalance" - onContinue={onDismiss} - onCancel={onMore} - /> - - ); -}; - -export default memo(HighBalanceWarning); diff --git a/src/navigation/bottom-sheet/LNURLWithdrawNavigation.tsx b/src/navigation/bottom-sheet/LNURLWithdrawNavigation.tsx deleted file mode 100644 index 54c726c8d..000000000 --- a/src/navigation/bottom-sheet/LNURLWithdrawNavigation.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { NavigationIndependentTree } from '@react-navigation/native'; -import { - NativeStackNavigationOptions, - NativeStackNavigationProp, - createNativeStackNavigator, -} from '@react-navigation/native-stack'; -import { LNURLWithdrawParams } from 'js-lnurl'; -import React, { ReactElement, memo } from 'react'; - -import BottomSheetWrapper from '../../components/BottomSheetWrapper'; -import { __E2E__ } from '../../constants/env'; -import { - useBottomSheetBackPress, - useSnapPoints, -} from '../../hooks/bottomSheet'; -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 BottomSheetNavigationContainer from './BottomSheetNavigationContainer'; - -export type LNURLWithdrawNavigationProp = - NativeStackNavigationProp; - -export type LNURLWithdrawStackParamList = { - Amount: { wParams: LNURLWithdrawParams }; - Confirm: { amount: number; wParams: LNURLWithdrawParams }; -}; - -const Stack = createNativeStackNavigator(); - -const screenOptions: NativeStackNavigationOptions = { - headerShown: false, - animation: __E2E__ ? 'none' : 'default', -}; - -const LNURLWithdrawNavigation = (): ReactElement => { - const snapPoints = useSnapPoints('large'); - const { isOpen, wParams } = useAppSelector((state) => { - return viewControllerSelector(state, 'lnurlWithdraw'); - }); - - useBottomSheetBackPress('lnurlWithdraw'); - - if (!wParams) { - return <>; - } - - // if max === min withdrawable amount, skip the Amount screen - const initialRouteName = - wParams.minWithdrawable === wParams.maxWithdrawable ? 'Confirm' : 'Amount'; - - return ( - - - - - - - - - - - ); -}; - -export default memo(LNURLWithdrawNavigation); diff --git a/src/navigation/bottom-sheet/PINNavigation.tsx b/src/navigation/bottom-sheet/PINNavigation.tsx deleted file mode 100644 index 80d4e57db..000000000 --- a/src/navigation/bottom-sheet/PINNavigation.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { NavigationIndependentTree } from '@react-navigation/native'; -import { - NativeStackNavigationOptions, - NativeStackNavigationProp, - createNativeStackNavigator, -} from '@react-navigation/native-stack'; -import React, { ReactElement, memo } from 'react'; -import { BiometryType } from 'react-native-biometrics'; - -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 BottomSheetNavigationContainer from './BottomSheetNavigationContainer'; - -export type PinNavigationProp = NativeStackNavigationProp; - -export type PinStackParamList = { - PINPrompt: { showLaterButton: boolean }; - ChoosePIN: { pin: string } | undefined; - AskForBiometrics: undefined; - Result: { bio: boolean; type: BiometryType }; -}; - -const Stack = createNativeStackNavigator(); - -const screenOptions: NativeStackNavigationOptions = { - headerShown: false, - animation: __E2E__ ? 'none' : 'default', -}; - -const PINNavigation = (): ReactElement => { - const snapPoints = useSnapPoints('medium'); - const isOpen = useAppSelector((state) => { - return viewControllerIsOpenSelector(state, 'PINNavigation'); - }); - - return ( - - - - - - - - - - - - - ); -}; - -export default memo(PINNavigation); diff --git a/src/navigation/bottom-sheet/QuickPayPrompt.tsx b/src/navigation/bottom-sheet/QuickPayPrompt.tsx deleted file mode 100644 index 2f7a12473..000000000 --- a/src/navigation/bottom-sheet/QuickPayPrompt.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useNavigation } from '@react-navigation/native'; -import React, { memo, ReactElement, useEffect, useMemo } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; - -import BottomSheetScreen from '../../components/BottomSheetScreen'; -import BottomSheetWrapper from '../../components/BottomSheetWrapper'; -import { __E2E__ } from '../../constants/env'; -import { - useBottomSheetBackPress, - useSnapPoints, -} from '../../hooks/bottomSheet'; -import { useAppDispatch, useAppSelector } from '../../hooks/redux'; -import { useBalance } from '../../hooks/wallet'; -import { quickpayIntroSeenSelector } from '../../store/reselect/settings'; -import { viewControllersSelector } from '../../store/reselect/ui'; -import { updateSettings } from '../../store/slices/settings'; -import { closeSheet } from '../../store/slices/ui'; -import { showBottomSheet } from '../../store/utils/ui'; -import { BodyMB, Display } from '../../styles/text'; -import { objectKeys } from '../../utils/objectKeys'; -import { RootNavigationProp } from '../types'; - -const imageSrc = require('../../assets/illustrations/fast-forward.png'); - -const CHECK_DELAY = 3000; // how long user needs to stay on Wallets screen before he will see this prompt - -const QuickPayPrompt = (): ReactElement => { - const { t } = useTranslation('settings'); - const navigation = useNavigation(); - const { spendingBalance } = useBalance(); - const snapPoints = useSnapPoints('large'); - const dispatch = useAppDispatch(); - const viewControllers = useAppSelector(viewControllersSelector); - const quickpayIntroSeen = useAppSelector(quickpayIntroSeenSelector); - - useBottomSheetBackPress('quickPay'); - - const anyBottomSheetIsOpen = useMemo(() => { - const viewControllerKeys = objectKeys(viewControllers); - return viewControllerKeys - .filter((view) => view !== 'quickPay') - .some((view) => viewControllers[view].isOpen); - }, [viewControllers]); - - // if user hasn't seen this prompt - // and has a spending balance - // and no other bottom-sheets are shown - // and user on "Wallets" screen for CHECK_DELAY - const shouldShowBottomSheet = useMemo(() => { - return ( - !__E2E__ && - !anyBottomSheetIsOpen && - !quickpayIntroSeen && - spendingBalance > 0 - ); - }, [anyBottomSheetIsOpen, quickpayIntroSeen, spendingBalance]); - - useEffect(() => { - if (!shouldShowBottomSheet) { - return; - } - - const timer = setTimeout(() => { - showBottomSheet('quickPay'); - }, CHECK_DELAY); - - return (): void => { - clearTimeout(timer); - }; - }, [shouldShowBottomSheet]); - - const onMore = (): void => { - navigation.navigate('Settings', { screen: 'QuickpaySettings' }); - dispatch(updateSettings({ quickpayIntroSeen: true })); - dispatch(closeSheet('quickPay')); - }; - - const onDismiss = (): void => { - dispatch(updateSettings({ quickpayIntroSeen: true })); - dispatch(closeSheet('quickPay')); - }; - - return ( - { - dispatch(updateSettings({ quickpayIntroSeen: true })); - }}> - }} - /> - } - description={ - }} - /> - } - image={imageSrc} - showBackButton={false} - continueText={t('learn_more')} - cancelText={t('later')} - testID="QuickPayPrompt" - onContinue={onMore} - onCancel={onDismiss} - /> - - ); -}; - -export default memo(QuickPayPrompt); diff --git a/src/navigation/bottom-sheet/ReceiveNavigation.tsx b/src/navigation/bottom-sheet/ReceiveNavigation.tsx deleted file mode 100644 index 2312bc585..000000000 --- a/src/navigation/bottom-sheet/ReceiveNavigation.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { NavigationIndependentTree } from '@react-navigation/native'; -import { - NativeStackNavigationOptions, - NativeStackNavigationProp, - createNativeStackNavigator, -} from '@react-navigation/native-stack'; -import React, { ReactElement, memo } from 'react'; - -import BottomSheetWrapper from '../../components/BottomSheetWrapper'; -import { __E2E__ } from '../../constants/env'; -import { useSnapPoints } from '../../hooks/bottomSheet'; -import { useAppDispatch, useAppSelector } from '../../hooks/redux'; -import Liquidity from '../../screens/Wallets/Receive/Liquidity'; -import ReceiveAmount from '../../screens/Wallets/Receive/ReceiveAmount'; -import ReceiveConnect from '../../screens/Wallets/Receive/ReceiveConnect'; -import ReceiveDetails from '../../screens/Wallets/Receive/ReceiveDetails'; -import ReceiveGeoBlocked from '../../screens/Wallets/Receive/ReceiveGeoBlocked'; -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 BottomSheetNavigationContainer from './BottomSheetNavigationContainer'; - -export type ReceiveNavigationProp = - NativeStackNavigationProp; - -export type ReceiveStackParamList = { - ReceiveQR: undefined; - ReceiveDetails: { - receiveAddress: string; - lightningInvoice?: string; - enableInstant?: boolean; - }; - Tags: undefined; - ReceiveAmount: undefined; - ReceiveGeoBlocked: undefined; - ReceiveConnect: { isAdditional: boolean } | undefined; - Liquidity: { - channelSize: number; - localBalance: number; - isAdditional: boolean; - }; -}; - -const Stack = createNativeStackNavigator(); -const screenOptions: NativeStackNavigationOptions = { - headerShown: false, - animation: __E2E__ ? 'none' : 'default', -}; - -const ReceiveNavigation = (): ReactElement => { - const snapPoints = useSnapPoints('large'); - const dispatch = useAppDispatch(); - const { isOpen, receiveScreen } = useAppSelector((state) => { - return viewControllerSelector(state, 'receiveNavigation'); - }); - - const initialRouteName = receiveScreen ?? 'ReceiveQR'; - - const reset = (): void => { - dispatch(resetInvoice()); - }; - - return ( - - - - - - - - - - - - - - - - ); -}; - -export default memo(ReceiveNavigation); diff --git a/src/navigation/bottom-sheet/SendNavigation.tsx b/src/navigation/bottom-sheet/SendNavigation.tsx deleted file mode 100644 index 6d234f2e3..000000000 --- a/src/navigation/bottom-sheet/SendNavigation.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import { - NavigationIndependentTree, - createNavigationContainerRef, -} from '@react-navigation/native'; -import { - NativeStackNavigationOptions, - NativeStackNavigationProp, - createNativeStackNavigator, -} from '@react-navigation/native-stack'; -import { LNURLPayParams } from 'js-lnurl'; -import React, { ReactElement, memo } from 'react'; - -import BottomSheetWrapper from '../../components/BottomSheetWrapper'; -import { __E2E__ } from '../../constants/env'; -import { useSnapPoints } from '../../hooks/bottomSheet'; -import { useLightningBalance } from '../../hooks/lightning'; -import { useAppSelector } from '../../hooks/redux'; -import LNURLAmount from '../../screens/Wallets/LNURLPay/Amount'; -import LNURLConfirm from '../../screens/Wallets/LNURLPay/Confirm'; -import Address from '../../screens/Wallets/Send/Address'; -import Amount from '../../screens/Wallets/Send/Amount'; -import AutoRebalance from '../../screens/Wallets/Send/AutoRebalance'; -import CoinSelection from '../../screens/Wallets/Send/CoinSelection'; -import Contacts from '../../screens/Wallets/Send/Contacts'; -import ErrorScreen from '../../screens/Wallets/Send/Error'; -import FeeCustom from '../../screens/Wallets/Send/FeeCustom'; -import FeeRate from '../../screens/Wallets/Send/FeeRate'; -import Pending from '../../screens/Wallets/Send/Pending'; -import PinCheck from '../../screens/Wallets/Send/PinCheck'; -import Quickpay from '../../screens/Wallets/Send/Quickpay'; -import Recipient from '../../screens/Wallets/Send/Recipient'; -import ReviewAndSend from '../../screens/Wallets/Send/ReviewAndSend'; -import Scanner from '../../screens/Wallets/Send/Scanner'; -import Success from '../../screens/Wallets/Send/Success'; -import Tags from '../../screens/Wallets/Send/Tags'; -import { - setupFeeForOnChainTransaction, - setupOnChainTransaction, -} from '../../store/actions/wallet'; -import { viewControllerSelector } from '../../store/reselect/ui'; -import { - selectedNetworkSelector, - selectedWalletSelector, - transactionSelector, -} from '../../store/reselect/wallet'; -import { EActivityType } from '../../store/types/activity'; -import { updateOnchainFeeEstimates } from '../../store/utils/fees'; -import { refreshLdk } from '../../utils/lightning'; -import BottomSheetNavigationContainer from './BottomSheetNavigationContainer'; - -export type SendNavigationProp = NativeStackNavigationProp; - -export type SendStackParamList = { - PinCheck: { onSuccess: () => void }; - Recipient: undefined; - Contacts: undefined; - Address: { uri?: string } | undefined; - Scanner: undefined; - Amount: undefined; - CoinSelection: undefined; - FeeRate: undefined; - FeeCustom: undefined; - ReviewAndSend: undefined; - Tags: undefined; - AutoRebalance: undefined; - Pending: { txId: string }; - Quickpay: { invoice: string; amount: number }; - Success: { type: EActivityType; amount: number; txId: string }; - Error: { errorMessage: string }; - LNURLAmount: { pParams: LNURLPayParams; url: string }; - LNURLConfirm: { amount: number; pParams: LNURLPayParams; url: string }; -}; - -const Stack = createNativeStackNavigator(); -const screenOptions: NativeStackNavigationOptions = { - headerShown: false, - animation: __E2E__ ? 'none' : 'default', -}; - -/** - * Helper function to navigate from outside components. - */ -export const navigationRef = createNavigationContainerRef(); -export const sendNavigation = { - navigate( - ...args: RouteName extends unknown - ? undefined extends SendStackParamList[RouteName] - ? - | [screen: RouteName] - | [screen: RouteName, params: SendStackParamList[RouteName]] - : [screen: RouteName, params: SendStackParamList[RouteName]] - : never - ): void { - if (navigationRef.isReady()) { - const currentRoute = navigationRef.getCurrentRoute()?.name; - const nextRoute = args[0]; - - if (currentRoute === nextRoute) { - console.log(`Already on screen ${currentRoute}. Skipping...`); - return; - } - - navigationRef.navigate(...args); - } else { - // sendNavigation not ready, try again after a short wait - setTimeout(() => sendNavigation.navigate(...args), 200); - } - }, -}; - -const SendNavigation = (): ReactElement => { - const snapPoints = useSnapPoints('large'); - const lightningBalance = useLightningBalance(false); - const selectedWallet = useAppSelector(selectedWalletSelector); - const selectedNetwork = useAppSelector(selectedNetworkSelector); - const { isOpen, screen, pParams, invoice, amount, url } = useAppSelector( - (state) => { - return viewControllerSelector(state, 'sendNavigation'); - }, - ); - const transaction = useAppSelector(transactionSelector); - - const initialRouteName = screen ?? 'Recipient'; - - const onOpen = async (): Promise => { - if (!transaction?.lightningInvoice) { - await updateOnchainFeeEstimates({ forceUpdate: true }); - if (!transaction?.inputs.length) { - await setupOnChainTransaction(); - } - setupFeeForOnChainTransaction(); - } - - if (lightningBalance.localBalance > 0) { - refreshLdk({ selectedWallet, selectedNetwork }).then(); - } - }; - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default memo(SendNavigation); diff --git a/src/navigation/root/RootNavigationContainer.tsx b/src/navigation/root/RootNavigationContainer.tsx index a1d365aaa..bec38b5a4 100644 --- a/src/navigation/root/RootNavigationContainer.tsx +++ b/src/navigation/root/RootNavigationContainer.tsx @@ -4,9 +4,10 @@ import { NavigationContainer, createNavigationContainerRef, } from '@react-navigation/native'; -import React, { ReactElement } from 'react'; +import React, { ReactElement, useEffect } from 'react'; import { Linking } from 'react-native'; +import { useAllSheetRefs } from '../../sheets/SheetRefsProvider'; import { processUri } from '../../utils/scanner/scanner'; import { RootStackParamList } from '../types'; @@ -48,6 +49,8 @@ const RootNavigationContainer = ({ }: { children: ReactElement; }): ReactElement => { + const sheetRefs = useAllSheetRefs(); + const linking: LinkingOptions = { prefixes: ['bitkit', 'slash', 'bitcoin', 'lightning'], subscribe(listener): () => void { @@ -64,6 +67,17 @@ const RootNavigationContainer = ({ }, }; + // Close any open sheets when navigating to a new screen + // biome-ignore lint/correctness/useExhaustiveDependencies: sheetRefs don't change + useEffect(() => { + const unsubscribe = navigationRef.addListener('state', () => { + const openSheets = sheetRefs.filter(({ ref }) => ref.current?.isOpen()); + openSheets.forEach(({ ref }) => ref.current?.close()); + }); + + return unsubscribe; + }, []); + return ( { const isAuthenticated = useAppSelector(isAuthenticatedSelector); const renderCount = useRenderCount(); + useBottomSheetBackPress(); + const checkClipboard = async (): Promise => { const result = await checkClipboardData(); if (result.isOk()) { diff --git a/src/navigation/types/index.ts b/src/navigation/types/index.ts index 0c8b11d98..a203f18f6 100644 --- a/src/navigation/types/index.ts +++ b/src/navigation/types/index.ts @@ -8,20 +8,20 @@ import { } from '@react-navigation/native-stack'; import type { RecoveryStackParamList } from '../../screens/Recovery/RecoveryNavigator'; +import type { BackupStackParamList } from '../../sheets/BackupNavigation'; +import type { LNURLWithdrawStackParamList } from '../../sheets/LNURLWithdrawNavigation'; +import type { OrangeTicketStackParamList } from '../../sheets/OrangeTicketNavigation'; +import type { PinStackParamList } from '../../sheets/PINNavigation'; +import type { ProfileLinkStackParamList } from '../../sheets/ProfileLinkNavigation'; +import type { ReceiveStackParamList } from '../../sheets/ReceiveNavigation'; +import type { SendStackParamList } from '../../sheets/SendNavigation'; +import type { TreasureHuntStackParamList } from '../../sheets/TreasureHuntNavigation'; 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'; -import type { PinStackParamList } from '../bottom-sheet/PINNavigation'; -import type { ProfileLinkStackParamList } from '../bottom-sheet/ProfileLinkNavigation'; -import type { ReceiveStackParamList } from '../bottom-sheet/ReceiveNavigation'; -import type { SendStackParamList } from '../bottom-sheet/SendNavigation'; -import type { TreasureHuntStackParamList } from '../bottom-sheet/TreasureHuntNavigation'; // TODO: move all navigation related types here // https://reactnavigation.org/docs/typescript#organizing-types @@ -95,8 +95,9 @@ export type ReceiveScreenProps = export type SendScreenProps = NativeStackScreenProps; -export type LNURLWithdrawProps = - NativeStackScreenProps; +export type LNURLWithdrawScreenProps< + T extends keyof LNURLWithdrawStackParamList, +> = NativeStackScreenProps; export type OrangeTicketScreenProps< T extends keyof OrangeTicketStackParamList, diff --git a/src/screens/Activity/ActivityDetail.tsx b/src/screens/Activity/ActivityDetail.tsx index fdf62f38f..56264aa9d 100644 --- a/src/screens/Activity/ActivityDetail.tsx +++ b/src/screens/Activity/ActivityDetail.tsx @@ -28,6 +28,7 @@ import Tag from '../../components/Tag'; import Button from '../../components/buttons/Button'; import useColors from '../../hooks/colors'; import { useAppDispatch, useAppSelector } from '../../hooks/redux'; +import ActivityTags from '../../sheets/ActivityTags'; import { EActivityType, TLightningActivityItem, @@ -59,7 +60,6 @@ import { canBoost, getBlockExplorerLink, } from '../../utils/wallet/transactions'; -import ActivityTagsPrompt from './ActivityTagsPrompt'; import { useOnchainWallet, useSwitchUnit } from '../../hooks/wallet'; import type { @@ -85,7 +85,7 @@ import { deleteMetaTxTag, } from '../../store/slices/metadata'; import { ETransferStatus } from '../../store/types/wallet'; -import { showBottomSheet } from '../../store/utils/ui'; +import { showSheet } from '../../store/utils/ui'; import { getBoostedTransactionParents } from '../../utils/boost'; import { ellipsis, @@ -235,11 +235,11 @@ const OnchainActivityDetail = ({ }; const handleBoost = (): void => { - showBottomSheet('boostPrompt', { onchainActivityItem: item }); + showSheet('boost', { activityItem: item }); }; const handleAddTag = (): void => { - showBottomSheet('activityTagsPrompt', { id }); + showSheet('activityTags', { id }); }; const handleRemoveTag = (tag: string): void => { @@ -700,7 +700,7 @@ const LightningActivityDetail = ({ }); const handleAddTag = (): void => { - showBottomSheet('activityTagsPrompt', { id }); + showSheet('activityTags', { id }); }; const handleRemoveTag = (tag: string): void => { @@ -1076,7 +1076,7 @@ const ActivityDetail = ({ )} - + ); }; diff --git a/src/screens/Activity/ActivityFiltered.tsx b/src/screens/Activity/ActivityFiltered.tsx index 46b80129f..344665474 100644 --- a/src/screens/Activity/ActivityFiltered.tsx +++ b/src/screens/Activity/ActivityFiltered.tsx @@ -13,14 +13,12 @@ import SafeAreaInset from '../../components/SafeAreaInset'; import SearchInput from '../../components/SearchInput'; import Tabs, { TTab } from '../../components/Tabs'; import Tag from '../../components/Tag'; -import { useAppDispatch } from '../../hooks/redux'; -import { closeSheet } from '../../store/slices/ui'; -import { showBottomSheet } from '../../store/utils/ui'; +import DatePicker from '../../sheets/DatePicker'; +import { useSheetRef } from '../../sheets/SheetRefsProvider'; +import TagsSheet from '../../sheets/Tags'; import { ScrollView, View as ThemedView } from '../../styles/components'; import { CalendarIcon, TagIcon } from '../../styles/icons'; import ActivityList from './ActivityList'; -import TagsPrompt from './TagsPrompt'; -import TimeRangePrompt from './TimeRangePrompt'; const tabs: TTab[] = [ { id: 'all', filter: { includeTransfers: true } }, @@ -48,8 +46,9 @@ const Glow = ({ const ActivityFiltered = (): ReactElement => { const { t } = useTranslation('wallet'); - const dispatch = useAppDispatch(); const size = useSharedValue({ width: 0, height: 0 }); + const tagsSheetRef = useSheetRef('tags'); + const datePickerSheetRef = useSheetRef('datePicker'); const panGestureRef = useRef(Gesture.Pan()); const [radiusContainerHeight, setRadiusContainerHeight] = useState(0); const [currentTab, setCurrentTab] = useState(0); @@ -67,8 +66,9 @@ const ActivityFiltered = (): ReactElement => { const addTag = (tag: string): void => { setTags((tg) => [...tg, tag]); - dispatch(closeSheet('tagsPrompt')); + tagsSheetRef.current?.close(); }; + const removeTag = (tag: string): void => { setTags((tg) => tg.filter((x) => x !== tag)); }; @@ -128,7 +128,7 @@ const ActivityFiltered = (): ReactElement => { testID="TagsPrompt" onPress={(): void => { Keyboard.dismiss(); - showBottomSheet('tagsPrompt'); + tagsSheetRef.current?.present(); }}> { { Keyboard.dismiss(); - showBottomSheet('timeRangePrompt'); + datePickerSheetRef.current?.present(); }}> { {/* TODO: move these up the tree, causing slow down when navigating */} - - + + ); }; diff --git a/src/screens/Activity/ActivityListShort.tsx b/src/screens/Activity/ActivityListShort.tsx index 03c9244f5..baab3043b 100644 --- a/src/screens/Activity/ActivityListShort.tsx +++ b/src/screens/Activity/ActivityListShort.tsx @@ -12,9 +12,9 @@ import { StyleSheet, View } from 'react-native'; import Button from '../../components/buttons/Button'; import { useAppSelector } from '../../hooks/redux'; import type { RootNavigationProp } from '../../navigation/types'; +import { useSheetRef } from '../../sheets/SheetRefsProvider'; import { activityItemsSelector } from '../../store/reselect/activity'; import { IActivityItem } from '../../store/types/activity'; -import { showBottomSheet } from '../../store/utils/ui'; import { Caption13Up } from '../../styles/text'; import { groupActivityItems } from '../../utils/activity'; import ListItem, { EmptyItem } from './ListItem'; @@ -24,6 +24,7 @@ const MAX_ACTIVITY_ITEMS = 3; const ActivityListShort = (): ReactElement => { const { t } = useTranslation('wallet'); const navigation = useNavigation(); + const sheetRef = useSheetRef('receive'); const items = useAppSelector(activityItemsSelector); const groupedItems = useMemo(() => { @@ -57,8 +58,9 @@ const ActivityListShort = (): ReactElement => { [navigation], ); + // biome-ignore lint/correctness/useExhaustiveDependencies: sheetRef doesn't change const navigateToReceive = useCallback((): void => { - showBottomSheet('receiveNavigation'); + sheetRef.current?.present(); }, []); const navigateToActivityFiltered = useCallback((): void => { diff --git a/src/screens/Activity/ActivityTagsPrompt.tsx b/src/screens/Activity/ActivityTagsPrompt.tsx deleted file mode 100644 index 2a8498730..000000000 --- a/src/screens/Activity/ActivityTagsPrompt.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import React, { memo, ReactElement, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { StyleSheet, View } from 'react-native'; - -import BottomSheetWrapper from '../../components/BottomSheetWrapper'; -import SafeAreaInset from '../../components/SafeAreaInset'; -import Tag from '../../components/Tag'; -import Button from '../../components/buttons/Button'; -import { - useBottomSheetBackPress, - useSnapPoints, -} from '../../hooks/bottomSheet'; -import { Keyboard } from '../../hooks/keyboard'; -import { useAppDispatch, useAppSelector } from '../../hooks/redux'; -import { lastUsedTagsSelector } from '../../store/reselect/metadata'; -import { viewControllerSelector } from '../../store/reselect/ui'; -import { addMetaTxTag } from '../../store/slices/metadata'; -import { closeSheet } from '../../store/slices/ui'; -import { BottomSheetTextInput } from '../../styles/components'; -import { Subtitle, Text13UP } from '../../styles/text'; - -const ActivityTagsPrompt = (): ReactElement => { - const { t } = useTranslation('wallet'); - const snapPoints = useSnapPoints('small'); - const [text, setText] = useState(''); - const dispatch = useAppDispatch(); - const lastUsedTags = useAppSelector(lastUsedTagsSelector); - const { isOpen, id } = useAppSelector((state) => { - return viewControllerSelector(state, 'activityTagsPrompt'); - }); - - useBottomSheetBackPress('activityTagsPrompt'); - - const closeBottomSheet = async (): Promise => { - setText(''); - await Keyboard.dismiss(); - dispatch(closeSheet('activityTagsPrompt')); - }; - - const handleTagChoose = async (tag: string): Promise => { - dispatch(addMetaTxTag({ txId: id!, tag: tag })); - closeBottomSheet(); - }; - - const handleSubmit = async (): Promise => { - if (text.length === 0) { - return; - } - dispatch(addMetaTxTag({ txId: id!, tag: text })); - closeBottomSheet(); - }; - - return ( - - - {t('tags_add')} - - {isOpen && ( - <> - {lastUsedTags.length !== 0 && ( - <> - - {t('tags_previously')} - - - {lastUsedTags.map((tag) => ( - { - handleTagChoose(tag); - }} - /> - ))} - - - )} - - - {t('tags_new')} - - - - -