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')}
-
-
-
-
-
-
- >
- )}
-
-
-
-
- );
-};
-
-const styles = StyleSheet.create({
- root: {
- flex: 1,
- paddingHorizontal: 16,
- },
- title: {
- marginBottom: 25,
- textAlign: 'center',
- },
- tagsContainer: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- marginBottom: 32,
- },
- tag: {
- marginRight: 8,
- marginBottom: 8,
- },
- label: {
- marginBottom: 16,
- },
- buttonContainer: {
- marginTop: 'auto',
- },
-});
-
-export default memo(ActivityTagsPrompt);
diff --git a/src/screens/Contacts/Contacts.tsx b/src/screens/Contacts/Contacts.tsx
index 0358bce0b..6b8a10911 100644
--- a/src/screens/Contacts/Contacts.tsx
+++ b/src/screens/Contacts/Contacts.tsx
@@ -1,27 +1,27 @@
+import { parse } from '@synonymdev/slashtags-url';
import React, { ReactElement, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Keyboard, StyleSheet, View } from 'react-native';
-import { useAppSelector } from '../../hooks/redux';
-import { parse } from '@synonymdev/slashtags-url';
import ContactsList from '../../components/ContactsList';
import NavigationHeader from '../../components/NavigationHeader';
import ProfileImage from '../../components/ProfileImage';
import SafeAreaInset from '../../components/SafeAreaInset';
import SearchInput from '../../components/SearchInput';
+import { useAppSelector } from '../../hooks/redux';
import { useProfile, useSlashtags } from '../../hooks/slashtags';
import { RootStackScreenProps } from '../../navigation/types';
+import AddContact from '../../sheets/AddContact';
+import { useSheetRef } from '../../sheets/SheetRefsProvider';
import {
contactsSelector,
onboardedContactsSelector,
} from '../../store/reselect/slashtags';
-import { showBottomSheet } from '../../store/utils/ui';
import {
View as ThemedView,
TouchableHighlight,
} from '../../styles/components';
import { PlusIcon } from '../../styles/icons';
-import AddContact from './AddContact';
import ContactsOnboarding from './ContactsOnboarding';
const Contacts = (props: RootStackScreenProps<'Contacts'>): ReactElement => {
@@ -40,6 +40,7 @@ const ContactsScreen = ({
navigation,
}: RootStackScreenProps<'Contacts'>): ReactElement => {
const { t } = useTranslation('slashtags');
+ const sheetRef = useSheetRef('addContact');
const [searchFilter, setSearchFilter] = useState('');
const { url: myProfileURL } = useSlashtags();
const { profile } = useProfile(myProfileURL);
@@ -73,7 +74,7 @@ const ContactsScreen = ({
testID="AddContact"
onPress={(): void => {
Keyboard.dismiss();
- showBottomSheet('addContactModal');
+ sheetRef.current?.present();
}}>
diff --git a/src/screens/OrangeTicket/Error.tsx b/src/screens/OrangeTicket/Error.tsx
index 213b15a59..eb111dc10 100644
--- a/src/screens/OrangeTicket/Error.tsx
+++ b/src/screens/OrangeTicket/Error.tsx
@@ -5,9 +5,8 @@ import BottomSheetNavigationHeader from '../../components/BottomSheetNavigationH
import GradientView from '../../components/GradientView';
import SafeAreaInset from '../../components/SafeAreaInset';
import Button from '../../components/buttons/Button';
-import { useAppDispatch } from '../../hooks/redux';
import type { OrangeTicketScreenProps } from '../../navigation/types';
-import { closeSheet } from '../../store/slices/ui';
+import { useSheetRef } from '../../sheets/SheetRefsProvider';
import { BodyM } from '../../styles/text';
const imageSrc = require('../../assets/illustrations/exclamation-mark.png');
@@ -35,12 +34,12 @@ const getText = (errorCode: number): { title: string; text: string } => {
const ErrorScreen = ({
route,
}: OrangeTicketScreenProps<'Error'>): ReactElement => {
- const dispatch = useAppDispatch();
const { errorCode } = route.params;
const { title, text } = getText(errorCode);
+ const sheetRef = useSheetRef('orangeTicket');
const onContinue = (): void => {
- dispatch(closeSheet('orangeTicket'));
+ sheetRef.current?.close();
};
return (
diff --git a/src/screens/OrangeTicket/UsedCard.tsx b/src/screens/OrangeTicket/UsedCard.tsx
index fe4eb41ac..cde16dfe6 100644
--- a/src/screens/OrangeTicket/UsedCard.tsx
+++ b/src/screens/OrangeTicket/UsedCard.tsx
@@ -6,9 +6,8 @@ import BottomSheetNavigationHeader from '../../components/BottomSheetNavigationH
import GradientView from '../../components/GradientView';
import SafeAreaInset from '../../components/SafeAreaInset';
import Button from '../../components/buttons/Button';
-import { useAppDispatch } from '../../hooks/redux';
import type { OrangeTicketScreenProps } from '../../navigation/types';
-import { closeSheet } from '../../store/slices/ui';
+import { useSheetRef } from '../../sheets/SheetRefsProvider';
import { BodyM } from '../../styles/text';
const imageSrc = require('../../assets/illustrations/exclamation-mark.png');
@@ -17,10 +16,10 @@ const UsedCard = ({
route,
}: OrangeTicketScreenProps<'UsedCard'>): ReactElement => {
const { amount } = route.params;
- const dispatch = useAppDispatch();
+ const sheetRef = useSheetRef('orangeTicket');
const onContinue = (): void => {
- dispatch(closeSheet('orangeTicket'));
+ sheetRef.current?.close();
};
return (
diff --git a/src/screens/Profile/ProfileEdit.tsx b/src/screens/Profile/ProfileEdit.tsx
index b5996d888..c8218949b 100644
--- a/src/screens/Profile/ProfileEdit.tsx
+++ b/src/screens/Profile/ProfileEdit.tsx
@@ -8,7 +8,6 @@ import React, {
} from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, View } from 'react-native';
-import { useAppDispatch, useAppSelector } from '../../hooks/redux';
import Dialog from '../../components/Dialog';
import Divider from '../../components/Divider';
@@ -19,9 +18,11 @@ import ProfileLinks from '../../components/ProfileLinks';
import SafeAreaInset from '../../components/SafeAreaInset';
import Button from '../../components/buttons/Button';
import { Keyboard } from '../../hooks/keyboard';
+import { useAppDispatch, useAppSelector } from '../../hooks/redux';
import { useProfile, useSlashtags } from '../../hooks/slashtags';
-import ProfileLinkNavigation from '../../navigation/bottom-sheet/ProfileLinkNavigation';
import type { RootStackScreenProps } from '../../navigation/types';
+import ProfileLinkNavigation from '../../sheets/ProfileLinkNavigation';
+import { useSheetRef } from '../../sheets/SheetRefsProvider';
import { slashtagsLinksSelector } from '../../store/reselect/slashtags';
import { onboardingProfileStepSelector } from '../../store/reselect/slashtags';
import {
@@ -29,7 +30,6 @@ import {
setOnboardingProfileStep,
} from '../../store/slices/slashtags';
import { BasicProfile } from '../../store/types/slashtags';
-import { showBottomSheet } from '../../store/utils/ui';
import { ScrollView, View as ThemedView } from '../../styles/components';
import { PlusIcon } from '../../styles/icons';
import { BodyS } from '../../styles/text';
@@ -48,6 +48,7 @@ const ProfileEdit = ({
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [fields, setFields] = useState>({});
const dispatch = useAppDispatch();
+ const sheetRef = useSheetRef('profileLink');
const links = useAppSelector(slashtagsLinksSelector);
const onboardingStep = useAppSelector(onboardingProfileStepSelector);
@@ -94,7 +95,7 @@ const ProfileEdit = ({
const onAddLink = async (): Promise => {
await Keyboard.dismiss();
- showBottomSheet('profileAddDataForm');
+ sheetRef.current?.present();
};
const onSave = async (): Promise => {
diff --git a/src/screens/Profile/ProfileLink.tsx b/src/screens/Profile/ProfileLink.tsx
index 8c4ed383d..6eac6d28d 100644
--- a/src/screens/Profile/ProfileLink.tsx
+++ b/src/screens/Profile/ProfileLink.tsx
@@ -6,13 +6,12 @@ import BottomSheetNavigationHeader from '../../components/BottomSheetNavigationH
import LabeledInput from '../../components/LabeledInput';
import SafeAreaInset from '../../components/SafeAreaInset';
import Button from '../../components/buttons/Button';
-import { useBottomSheetBackPress } from '../../hooks/bottomSheet';
-import { Keyboard } from '../../hooks/keyboard';
import { useAppDispatch, useAppSelector } from '../../hooks/redux';
import { ProfileLinkScreenProps } from '../../navigation/types';
+import { useSheetRef } from '../../sheets/SheetRefsProvider';
import { profileLinkSelector } from '../../store/reselect/ui';
import { addLink } from '../../store/slices/slashtags';
-import { closeSheet, updateProfileLink } from '../../store/slices/ui';
+import { updateProfileLink } from '../../store/slices/ui';
import { BodyS, BodySB } from '../../styles/text';
import { suggestions } from './ProfileLinkSuggestions';
@@ -21,15 +20,13 @@ const ProfileLink = ({
}: ProfileLinkScreenProps<'ProfileLink'>): ReactElement => {
const { t } = useTranslation('slashtags');
const dispatch = useAppDispatch();
+ const sheetRef = useSheetRef('profileLink');
const form = useAppSelector(profileLinkSelector);
- useBottomSheetBackPress('profileAddDataForm');
-
- const onSave = async (): Promise => {
+ const onSave = (): void => {
+ sheetRef.current?.close();
dispatch(addLink(form));
dispatch(updateProfileLink({ title: '', url: '' }));
- await Keyboard.dismiss();
- dispatch(closeSheet('profileAddDataForm'));
};
const isValid = form.title && form.url;
diff --git a/src/screens/Settings/AddressViewer/index.tsx b/src/screens/Settings/AddressViewer/index.tsx
index bb33bde3c..96047e333 100644
--- a/src/screens/Settings/AddressViewer/index.tsx
+++ b/src/screens/Settings/AddressViewer/index.tsx
@@ -15,18 +15,18 @@ import React, {
import { useTranslation } from 'react-i18next';
import { FlatList, ScrollView, StyleSheet, View } from 'react-native';
import QRCode from 'react-native-qrcode-svg';
-import { useAppDispatch, useAppSelector } from '../../../hooks/redux';
import NavigationHeader from '../../../components/NavigationHeader';
import SafeAreaInset from '../../../components/SafeAreaInset';
import SearchInput from '../../../components/SearchInput';
import Button from '../../../components/buttons/Button';
+import { useAppDispatch, useAppSelector } from '../../../hooks/redux';
+import { useSheetRef } from '../../../sheets/SheetRefsProvider';
import {
resetSendTransaction,
setupOnChainTransaction,
updateBeignetSendTransaction,
} from '../../../store/actions/wallet';
-import { viewControllerIsOpenSelector } from '../../../store/reselect/ui';
import {
addressTypeSelector,
currentWalletSelector,
@@ -43,7 +43,7 @@ import { updateWallet } from '../../../store/slices/wallet';
import { TWalletName } from '../../../store/types/wallet';
import { updateActivityList } from '../../../store/utils/activity';
import { updateOnchainFeeEstimates } from '../../../store/utils/fees';
-import { showBottomSheet } from '../../../store/utils/ui';
+import { showSheet } from '../../../store/utils/ui';
import {
View as ThemedView,
TouchableOpacity,
@@ -226,6 +226,7 @@ const getAllAddresses = async ({
const AddressViewer = (): ReactElement => {
const { t } = useTranslation('settings');
const dispatch = useAppDispatch();
+ const sendSheetRef = useSheetRef('send');
const selectedWallet = useAppSelector(selectedWalletSelector);
const selectedNetwork = useAppSelector(selectedNetworkSelector);
const addressType = useAppSelector(addressTypeSelector);
@@ -233,9 +234,7 @@ const AddressViewer = (): ReactElement => {
currentWalletSelector(state, selectedWallet),
);
const [sendNavigationHasOpened, setSendNavigationHasOpened] = useState(false);
- const sendNavigationIsOpen = useAppSelector((state) =>
- viewControllerIsOpenSelector(state, 'sendNavigation'),
- );
+ const sendNavigationIsOpen = sendSheetRef.current?.isOpen();
const flatListRef = useRef(null);
const scrollViewRef = useRef(null);
@@ -650,7 +649,7 @@ const AddressViewer = (): ReactElement => {
}),
);
sendMax();
- showBottomSheet('sendNavigation', { screen: 'ReviewAndSend' });
+ showSheet('send', { screen: 'ReviewAndSend' });
},
[selectedUtxos, utxos, selectedNetwork, dispatch],
);
diff --git a/src/screens/Settings/Backup/Metadata.tsx b/src/screens/Settings/Backup/Metadata.tsx
index b26fefe4e..b5160d22d 100644
--- a/src/screens/Settings/Backup/Metadata.tsx
+++ b/src/screens/Settings/Backup/Metadata.tsx
@@ -6,9 +6,9 @@ import BottomSheetNavigationHeader from '../../../components/BottomSheetNavigati
import GradientView from '../../../components/GradientView';
import SafeAreaInset from '../../../components/SafeAreaInset';
import Button from '../../../components/buttons/Button';
-import { useAppDispatch, useAppSelector } from '../../../hooks/redux';
+import { useAppSelector } from '../../../hooks/redux';
+import { useSheetRef } from '../../../sheets/SheetRefsProvider';
import { backupSelector } from '../../../store/reselect/backup';
-import { closeSheet } from '../../../store/slices/ui';
import { EBackupCategory } from '../../../store/types/backup';
import { BodyM, BodyS, BodySB } from '../../../styles/text';
import { i18nTime } from '../../../utils/i18n';
@@ -18,7 +18,7 @@ const imageSrc = require('../../../assets/illustrations/card.png');
const Metadata = (): ReactElement => {
const { t } = useTranslation('security');
const { t: tTime } = useTranslation('intl', { i18n: i18nTime });
- const dispatch = useAppDispatch();
+ const sheetRef = useSheetRef('backupNavigation');
const backup = useAppSelector(backupSelector);
const max = Math.max(
@@ -27,8 +27,8 @@ const Metadata = (): ReactElement => {
}),
);
- const handleButtonPress = (): void => {
- dispatch(closeSheet('backupNavigation'));
+ const onContinue = (): void => {
+ sheetRef.current?.close();
};
return (
@@ -67,12 +67,7 @@ const Metadata = (): ReactElement => {
/>
)}
-
+
diff --git a/src/screens/Settings/Backup/ResetAndRestore.tsx b/src/screens/Settings/Backup/ResetAndRestore.tsx
index c8588003a..000bbf7d5 100644
--- a/src/screens/Settings/Backup/ResetAndRestore.tsx
+++ b/src/screens/Settings/Backup/ResetAndRestore.tsx
@@ -6,8 +6,8 @@ import Dialog from '../../../components/Dialog';
import NavigationHeader from '../../../components/NavigationHeader';
import SafeAreaInset from '../../../components/SafeAreaInset';
import Button from '../../../components/buttons/Button';
+import { useSheetRef } from '../../../sheets/SheetRefsProvider';
import { wipeApp } from '../../../store/utils/settings';
-import { showBottomSheet } from '../../../store/utils/ui';
import { View } from '../../../styles/components';
import { BodyM } from '../../../styles/text';
@@ -15,6 +15,7 @@ const imageSrc = require('../../../assets/illustrations/restore.png');
const ResetAndRestore = (): ReactElement => {
const { t } = useTranslation('security');
+ const sheetRef = useSheetRef('backupNavigation');
const [showDialog, setShowDialog] = useState(false);
return (
@@ -35,7 +36,7 @@ const ResetAndRestore = (): ReactElement => {
style={styles.button}
text={t('reset_button_backup')}
onPress={(): void => {
- showBottomSheet('backupNavigation');
+ sheetRef.current?.present();
}}
/>
);
};
diff --git a/src/sheets/AppUpdate.tsx b/src/sheets/AppUpdate.tsx
new file mode 100644
index 000000000..7fd5c2273
--- /dev/null
+++ b/src/sheets/AppUpdate.tsx
@@ -0,0 +1,97 @@
+import React, { memo, ReactElement, useEffect } from 'react';
+import { Trans, useTranslation } from 'react-i18next';
+
+import BottomSheet from '../components/BottomSheet';
+import BottomSheetScreen from '../components/BottomSheetScreen';
+import { __E2E__ } from '../constants/env';
+import { useAppDispatch, useAppSelector } from '../hooks/redux';
+import { availableUpdateSelector } from '../store/reselect/ui';
+import { ignoreAppUpdateTimestampSelector } from '../store/reselect/user';
+import { ignoreAppUpdate } from '../store/slices/user';
+import { Display } from '../styles/text';
+import { openURL } from '../utils/helpers';
+import { useAllSheetRefs, useSheetRef } from './SheetRefsProvider';
+
+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 the home screen before he will see this prompt
+
+const sheetId = 'appUpdate';
+
+const AppUpdate = (): ReactElement => {
+ const { t } = useTranslation('other');
+ const dispatch = useAppDispatch();
+ const sheetRefs = useAllSheetRefs();
+ const sheetRef = useSheetRef(sheetId);
+ const updateInfo = useAppSelector(availableUpdateSelector);
+ const ignoreTimestamp = useAppSelector(ignoreAppUpdateTimestampSelector);
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies: sheetRefs don't change
+ useEffect(() => {
+ // 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 home screen for CHECK_DELAY
+ const shouldShow = () => {
+ const isTimeoutOver = Number(new Date()) - ignoreTimestamp > ASK_INTERVAL;
+ const isAnySheetOpen = sheetRefs.some(({ ref }) => ref.current?.isOpen());
+
+ return (
+ !__E2E__ &&
+ !isAnySheetOpen &&
+ isTimeoutOver &&
+ updateInfo !== null &&
+ !updateInfo.critical
+ );
+ };
+
+ const timer = setTimeout(() => {
+ if (shouldShow()) {
+ sheetRef.current?.present();
+ }
+ }, CHECK_DELAY);
+
+ return () => clearTimeout(timer);
+ }, [ignoreTimestamp, updateInfo]);
+
+ const onClose = (): void => {
+ dispatch(ignoreAppUpdate());
+ };
+
+ const onCancel = (): void => {
+ dispatch(ignoreAppUpdate());
+ sheetRef.current?.close();
+ };
+
+ const onUpdate = async (): Promise => {
+ dispatch(ignoreAppUpdate());
+ await openURL(updateInfo?.url!);
+ sheetRef.current?.close();
+ };
+
+ return (
+
+ }}
+ />
+ }
+ description={t('update_text')}
+ image={imageSrc}
+ showBackButton={false}
+ continueText={t('update_button')}
+ cancelText={t('cancel')}
+ testID="AppUpdate"
+ onContinue={onUpdate}
+ onCancel={onCancel}
+ />
+
+ );
+};
+
+export default memo(AppUpdate);
diff --git a/src/navigation/bottom-sheet/BackupNavigation.tsx b/src/sheets/BackupNavigation.tsx
similarity index 60%
rename from src/navigation/bottom-sheet/BackupNavigation.tsx
rename to src/sheets/BackupNavigation.tsx
index 19d809ab2..2d76aaf35 100644
--- a/src/navigation/bottom-sheet/BackupNavigation.tsx
+++ b/src/sheets/BackupNavigation.tsx
@@ -6,19 +6,16 @@ import {
} 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 { useAppSelector } from '../../hooks/redux';
-import ConfirmMnemonic from '../../screens/Settings/Backup/ConfirmMnemonic';
-import ConfirmPassphrase from '../../screens/Settings/Backup/ConfirmPassphrase';
-import Metadata from '../../screens/Settings/Backup/Metadata';
-import MultipleDevices from '../../screens/Settings/Backup/MultipleDevices';
-import ShowMnemonic from '../../screens/Settings/Backup/ShowMnemonic';
-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 BottomSheet from '../components/BottomSheet';
+import { __E2E__ } from '../constants/env';
+import ConfirmMnemonic from '../screens/Settings/Backup/ConfirmMnemonic';
+import ConfirmPassphrase from '../screens/Settings/Backup/ConfirmPassphrase';
+import Metadata from '../screens/Settings/Backup/Metadata';
+import MultipleDevices from '../screens/Settings/Backup/MultipleDevices';
+import ShowMnemonic from '../screens/Settings/Backup/ShowMnemonic';
+import ShowPassphrase from '../screens/Settings/Backup/ShowPassphrase';
+import Success from '../screens/Settings/Backup/Success';
+import Warning from '../screens/Settings/Backup/Warning';
import BottomSheetNavigationContainer from './BottomSheetNavigationContainer';
export type BackupNavigationProp =
@@ -43,15 +40,10 @@ const navOptions: NativeStackNavigationOptions = {
};
const BackupNavigation = (): ReactElement => {
- const snapPoints = useSnapPoints('medium');
- const isOpen = useAppSelector((state) => {
- return viewControllerIsOpenSelector(state, 'backupNavigation');
- });
-
return (
-
+
-
+
@@ -67,7 +59,7 @@ const BackupNavigation = (): ReactElement => {
-
+
);
};
diff --git a/src/sheets/BackupPrompt.tsx b/src/sheets/BackupPrompt.tsx
new file mode 100644
index 000000000..45d542350
--- /dev/null
+++ b/src/sheets/BackupPrompt.tsx
@@ -0,0 +1,103 @@
+import React, { memo, ReactElement, useEffect } from 'react';
+import { Trans, useTranslation } from 'react-i18next';
+
+import BottomSheet from '../components/BottomSheet';
+import BottomSheetScreen from '../components/BottomSheetScreen';
+import { __E2E__ } from '../constants/env';
+import { useAppDispatch, useAppSelector } from '../hooks/redux';
+import { useBalance } from '../hooks/wallet';
+import { backupVerifiedSelector } from '../store/reselect/user';
+import { ignoreBackupTimestampSelector } from '../store/reselect/user';
+import { ignoreBackup } from '../store/slices/user';
+import { Display } from '../styles/text';
+import { useAllSheetRefs, useSheetRef } from './SheetRefsProvider';
+
+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 = 1800; // how long user needs to stay on the home screen before he will see this prompt
+
+const sheetId = 'backupPrompt';
+
+const BackupPrompt = (): ReactElement => {
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation('security');
+ const sheetRefs = useAllSheetRefs();
+ const sheetRef = useSheetRef(sheetId);
+ const backupNavigationSheetRef = useSheetRef('backupNavigation');
+ const ignoreTimestamp = useAppSelector(ignoreBackupTimestampSelector);
+ const backupVerified = useAppSelector(backupVerifiedSelector);
+ const { totalBalance } = useBalance();
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies: sheetRefs don't change
+ useEffect(() => {
+ // if backup has not been verified
+ // and wallet has balance
+ // and user has not seen this prompt for ASK_INTERVAL
+ // and no other bottom-sheets are shown
+ // and user on home screen for CHECK_DELAY
+ const shouldShow = () => {
+ const isAnySheetOpen = sheetRefs.some(({ ref }) => ref.current?.isOpen());
+ const isTimeoutOver = Number(new Date()) - ignoreTimestamp > ASK_INTERVAL;
+ const hasBalance = totalBalance > 0;
+
+ return (
+ !__E2E__ &&
+ !isAnySheetOpen &&
+ isTimeoutOver &&
+ !backupVerified &&
+ hasBalance
+ );
+ };
+
+ const timer = setTimeout(() => {
+ if (shouldShow()) {
+ sheetRef.current?.present();
+ }
+ }, CHECK_DELAY);
+
+ return () => clearTimeout(timer);
+ }, [ignoreTimestamp, backupVerified, totalBalance]);
+
+ const handleLater = (): void => {
+ dispatch(ignoreBackup());
+ sheetRef.current?.close();
+ };
+
+ const handleBackup = (): void => {
+ sheetRef.current?.close();
+ backupNavigationSheetRef.current?.present();
+ };
+
+ 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/screens/Wallets/BoostPrompt.tsx b/src/sheets/Boost.tsx
similarity index 68%
rename from src/screens/Wallets/BoostPrompt.tsx
rename to src/sheets/Boost.tsx
index c570aa498..bc6762b1b 100644
--- a/src/screens/Wallets/BoostPrompt.tsx
+++ b/src/sheets/Boost.tsx
@@ -3,52 +3,49 @@ import React, { memo, ReactElement, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, StyleSheet, View } from 'react-native';
-import AdjustValue from '../../components/AdjustValue';
-import BottomSheetNavigationHeader from '../../components/BottomSheetNavigationHeader';
-import BottomSheetWrapper from '../../components/BottomSheetWrapper';
-import ImageText from '../../components/ImageText';
-import Money from '../../components/Money';
-import SafeAreaInset from '../../components/SafeAreaInset';
-import SwipeToConfirm from '../../components/SwipeToConfirm';
-import Button from '../../components/buttons/Button';
-import {
- useBottomSheetBackPress,
- useSnapPoints,
-} from '../../hooks/bottomSheet';
-import { useFeeText } from '../../hooks/fees';
-import { useAppDispatch, useAppSelector } from '../../hooks/redux';
-import { rootNavigation } from '../../navigation/root/RootNavigationContainer';
-import { resetSendTransaction } from '../../store/actions/wallet';
-import { viewControllerSelector } from '../../store/reselect/ui';
-import { transactionSelector } from '../../store/reselect/wallet';
-import { closeSheet } from '../../store/slices/ui';
-import { TOnchainActivityItem } from '../../store/types/activity';
-import { EUnit } from '../../store/types/wallet';
-import colors from '../../styles/colors';
-import { TimerIconAlt } from '../../styles/icons';
-import { BodyMSB, BodyS, BodySSB } from '../../styles/text';
-import { showToast } from '../../utils/notifications';
+import AdjustValue from '../components/AdjustValue';
+import BottomSheet from '../components/BottomSheet';
+import BottomSheetNavigationHeader from '../components/BottomSheetNavigationHeader';
+import ImageText from '../components/ImageText';
+import Money from '../components/Money';
+import SafeAreaInset from '../components/SafeAreaInset';
+import SwipeToConfirm from '../components/SwipeToConfirm';
+import Button from '../components/buttons/Button';
+import { useFeeText } from '../hooks/fees';
+import { useAppSelector } from '../hooks/redux';
+import { rootNavigation } from '../navigation/root/RootNavigationContainer';
+import { resetSendTransaction } from '../store/actions/wallet';
+import { transactionSelector } from '../store/reselect/wallet';
+import { SheetsParamList } from '../store/types/ui';
+import { EUnit } from '../store/types/wallet';
+import colors from '../styles/colors';
+import { TimerIconAlt } from '../styles/icons';
+import { BodyMSB, BodyS, BodySSB } from '../styles/text';
+import { showToast } from '../utils/notifications';
import {
adjustFee,
broadcastBoost,
canBoost,
setupBoost,
updateFee,
-} from '../../utils/wallet/transactions';
+} from '../utils/wallet/transactions';
+import { useSheetRef } from './SheetRefsProvider';
+
+const sheetId = 'boost';
-const BoostForm = ({
- activityItem,
-}: {
- activityItem: TOnchainActivityItem;
-}): ReactElement => {
+const SheetContent = ({
+ data,
+}: { data: SheetsParamList['boost'] }): ReactElement => {
+ const { activityItem } = data;
const { t } = useTranslation('wallet');
- const dispatch = useAppDispatch();
+ const sheetRef = useSheetRef(sheetId);
const transaction = useAppSelector(transactionSelector);
const [preparing, setPreparing] = useState(true);
const [loading, setLoading] = useState(false);
const [showCustom, setShowCustom] = useState(false);
const [origFee, setOrigFee] = useState(1);
+
const boostData = useMemo(
() => canBoost(activityItem.txId),
[activityItem.txId],
@@ -60,6 +57,7 @@ const BoostForm = ({
return boostData.canBoost ? transaction.fee : 0;
}, [boostData.canBoost, transaction.fee]);
+ // biome-ignore lint/correctness/useExhaustiveDependencies: sheetRef doesn't change
useEffect(() => {
(async (): Promise => {
const res = await setupBoost({
@@ -67,7 +65,7 @@ const BoostForm = ({
});
setPreparing(false);
if (res.isErr()) {
- dispatch(closeSheet('boostPrompt'));
+ sheetRef.current?.close();
return;
}
setOrigFee(res.value.satsPerByte!);
@@ -76,7 +74,7 @@ const BoostForm = ({
return (): void => {
resetSendTransaction();
};
- }, [activityItem.txId, dispatch]);
+ }, [activityItem.txId]);
const onSwitchView = (): void => {
if (showCustom) {
@@ -132,7 +130,7 @@ const BoostForm = ({
oldTxId: activityItem.txId,
});
if (response.isOk()) {
- dispatch(closeSheet('boostPrompt'));
+ sheetRef.current?.close();
showToast({
type: 'success',
title: t('boost_success_title'),
@@ -190,7 +188,11 @@ const BoostForm = ({
);
return (
- <>
+
+
{t(showCustom ? 'boost_fee_custom' : 'boost_fee_recomended')}
@@ -240,34 +242,18 @@ const BoostForm = ({
/>
- >
+
+
);
};
-const BoostPrompt = (): ReactElement => {
- const { t } = useTranslation('wallet');
- const snapPoints = useSnapPoints('small');
- const { isOpen, onchainActivityItem } = useAppSelector((state) => {
- return viewControllerSelector(state, 'boostPrompt');
- });
-
- useBottomSheetBackPress('boostPrompt');
-
+const BoostSheet = (): ReactElement => {
return (
-
-
-
-
- {isOpen && onchainActivityItem && (
-
- )}
-
-
-
-
+
+ {({ data }: { data: SheetsParamList['boost'] }) => {
+ return ;
+ }}
+
);
};
@@ -303,4 +289,4 @@ const styles = StyleSheet.create({
},
});
-export default memo(BoostPrompt);
+export default memo(BoostSheet);
diff --git a/src/navigation/bottom-sheet/BottomSheetNavigationContainer.tsx b/src/sheets/BottomSheetNavigationContainer.tsx
similarity index 100%
rename from src/navigation/bottom-sheet/BottomSheetNavigationContainer.tsx
rename to src/sheets/BottomSheetNavigationContainer.tsx
diff --git a/src/sheets/BottomSheets.tsx b/src/sheets/BottomSheets.tsx
new file mode 100644
index 000000000..1bc7f2f63
--- /dev/null
+++ b/src/sheets/BottomSheets.tsx
@@ -0,0 +1,34 @@
+import React, { JSX, memo } from 'react';
+
+import BackupNavigation from './BackupNavigation';
+import Boost from './Boost';
+import ConnectionClosed from './ConnectionClosed';
+import LNURLWithdrawNavigation from './LNURLWithdrawNavigation';
+import OrangeTicketNavigation from './OrangeTicketNavigation';
+import PINNavigation from './PINNavigation';
+// import TransferFailed from './TransferFailed';
+// import TreasureHuntNavigation from './TreasureHuntNavigation';
+import PubkyAuth from './PubkyAuth';
+import ReceiveNavigation from './ReceiveNavigation';
+import ReceivedTransaction from './ReceivedTransaction';
+import SendNavigation from './SendNavigation';
+
+const BottomSheets = (): JSX.Element => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {/* */}
+
+ >
+ );
+};
+
+export default memo(BottomSheets);
diff --git a/src/navigation/bottom-sheet/BottomSheetsLazy.tsx b/src/sheets/BottomSheetsLazy.tsx
similarity index 100%
rename from src/navigation/bottom-sheet/BottomSheetsLazy.tsx
rename to src/sheets/BottomSheetsLazy.tsx
diff --git a/src/navigation/bottom-sheet/ConnectionClosed.tsx b/src/sheets/ConnectionClosed.tsx
similarity index 61%
rename from src/navigation/bottom-sheet/ConnectionClosed.tsx
rename to src/sheets/ConnectionClosed.tsx
index 4b011e6cb..232d04010 100644
--- a/src/navigation/bottom-sheet/ConnectionClosed.tsx
+++ b/src/sheets/ConnectionClosed.tsx
@@ -2,33 +2,25 @@ import React, { memo, ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import { Image, StyleSheet, View } from 'react-native';
-import BottomSheetNavigationHeader from '../../components/BottomSheetNavigationHeader';
-import BottomSheetWrapper from '../../components/BottomSheetWrapper';
-import SafeAreaInset from '../../components/SafeAreaInset';
-import Button from '../../components/buttons/Button';
-import {
- useBottomSheetBackPress,
- useSnapPoints,
-} from '../../hooks/bottomSheet';
-import { useAppDispatch } from '../../hooks/redux';
-import { closeSheet } from '../../store/slices/ui';
-import { BodyM } from '../../styles/text';
+import BottomSheet from '../components/BottomSheet';
+import BottomSheetNavigationHeader from '../components/BottomSheetNavigationHeader';
+import SafeAreaInset from '../components/SafeAreaInset';
+import Button from '../components/buttons/Button';
+import { BodyM } from '../styles/text';
+import { useSheetRef } from './SheetRefsProvider';
-const imageSrc = require('../../assets/illustrations/switch.png');
+const imageSrc = require('../assets/illustrations/switch.png');
const ConnectionClosed = (): ReactElement => {
const { t } = useTranslation('lightning');
- const dispatch = useAppDispatch();
- const snapPoints = useSnapPoints('medium');
-
- useBottomSheetBackPress('backupPrompt');
+ const sheetRef = useSheetRef('connectionClosed');
const onContinue = (): void => {
- dispatch(closeSheet('connectionClosed'));
+ sheetRef.current?.close();
};
return (
-
+
{
-
+
);
};
diff --git a/src/screens/Activity/TimeRangePrompt.tsx b/src/sheets/DatePicker.tsx
similarity index 86%
rename from src/screens/Activity/TimeRangePrompt.tsx
rename to src/sheets/DatePicker.tsx
index fa1364cbd..9b7f6595d 100644
--- a/src/screens/Activity/TimeRangePrompt.tsx
+++ b/src/sheets/DatePicker.tsx
@@ -2,23 +2,20 @@ import React, { memo, ReactElement, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
-import BottomSheetWrapper from '../../components/BottomSheetWrapper';
-import SafeAreaInset from '../../components/SafeAreaInset';
-import Button from '../../components/buttons/Button';
-import {
- useBottomSheetBackPress,
- useSnapPoints,
-} from '../../hooks/bottomSheet';
-import { useAppDispatch, useAppSelector } from '../../hooks/redux';
-import { languageSelector, timeZoneSelector } from '../../store/reselect/ui';
-import { closeSheet } from '../../store/slices/ui';
-import { View as ThemedView } from '../../styles/components';
-import { LeftSign, RightSign } from '../../styles/icons';
-import { BodyMSB, Caption13Up, Subtitle } from '../../styles/text';
-import { generateCalendar } from '../../utils/helpers';
-import { i18nTime } from '../../utils/i18n';
+import BottomSheet from '../components/BottomSheet';
+import SafeAreaInset from '../components/SafeAreaInset';
+import Button from '../components/buttons/Button';
+import { useAppSelector } from '../hooks/redux';
+import { languageSelector, timeZoneSelector } from '../store/reselect/ui';
+import { View as ThemedView } from '../styles/components';
+import { LeftSign, RightSign } from '../styles/icons';
+import { BodyMSB, Caption13Up, Subtitle } from '../styles/text';
+import { generateCalendar } from '../utils/helpers';
+import { i18nTime } from '../utils/i18n';
+import { useSheetRef } from './SheetRefsProvider';
const DAY_HEIGHT = 44;
+const sheetId = 'datePicker';
const Day = ({
day,
@@ -91,20 +88,28 @@ const Day = ({
};
const Calendar = ({
+ initialRange,
onChange,
}: {
+ initialRange: number[];
onChange: (timeRange: number[]) => void;
}): ReactElement => {
const { t } = useTranslation('wallet');
const { t: tTime } = useTranslation('intl', { i18n: i18nTime });
- const dispatch = useAppDispatch();
+ const sheetRef = useSheetRef(sheetId);
const timeZone = useAppSelector(timeZoneSelector);
const language = useAppSelector(languageSelector);
const [monthDate, setMonthDate] = useState(() => {
+ if (initialRange.length > 0) {
+ const initialDate = new Date(initialRange[0]);
+ return new Date(initialDate.getFullYear(), initialDate.getMonth());
+ }
const n = new Date();
return new Date(n.getFullYear(), n.getMonth());
});
- const [range, setRange] = useState>([]);
+ const [range, setRange] = useState(
+ initialRange.map((t) => new Date(t)),
+ );
const { calendar, weekDays } = useMemo(() => {
const c = generateCalendar(monthDate, language, timeZone);
@@ -154,7 +159,7 @@ const Calendar = ({
const begin = range[0].getTime();
const end = (range[1] ?? range[0]).getTime() + 1000 * 60 * 60 * 24; // 24 hours
onChange([begin, end]);
- dispatch(closeSheet('timeRangePrompt'));
+ sheetRef.current?.close();
};
return (
@@ -292,34 +297,25 @@ const Calendar = ({
);
};
-const TimeRangePrompt = ({
+const DatePicker = ({
+ range,
onChange,
}: {
+ range: number[];
onChange: (timeRange: number[]) => void;
}): ReactElement => {
const { t } = useTranslation('wallet');
- const snapPoints = useSnapPoints('calendar');
- const dispatch = useAppDispatch();
-
- useBottomSheetBackPress('timeRangePrompt');
-
- const handleClose = (): void => {
- dispatch(closeSheet('timeRangePrompt'));
- };
return (
-
+
{t('filter_title')}
-
+
-
+
);
};
@@ -412,4 +408,4 @@ const styles = StyleSheet.create({
},
});
-export default memo(TimeRangePrompt);
+export default memo(DatePicker);
diff --git a/src/navigation/bottom-sheet/ForceTransfer.tsx b/src/sheets/ForceTransfer.tsx
similarity index 72%
rename from src/navigation/bottom-sheet/ForceTransfer.tsx
rename to src/sheets/ForceTransfer.tsx
index 3450ec8f0..2eaa9fb02 100644
--- a/src/navigation/bottom-sheet/ForceTransfer.tsx
+++ b/src/sheets/ForceTransfer.tsx
@@ -1,36 +1,30 @@
import React, { memo, ReactElement, useEffect, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
-import BottomSheetScreen from '../../components/BottomSheetScreen';
-import BottomSheetWrapper from '../../components/BottomSheetWrapper';
-import {
- useBottomSheetBackPress,
- useSnapPoints,
-} from '../../hooks/bottomSheet';
-import { useAppDispatch, useAppSelector } from '../../hooks/redux';
-import { startCoopCloseTimestampSelector } from '../../store/reselect/user';
-import { closeSheet } from '../../store/slices/ui';
-import { clearCoopCloseTimer } from '../../store/slices/user';
-import { showBottomSheet } from '../../store/utils/ui';
-import { Display } from '../../styles/text';
-import { closeAllChannels } from '../../utils/lightning';
-import { showToast } from '../../utils/notifications';
-
-const imageSrc = require('../../assets/illustrations/exclamation-mark.png');
+import BottomSheet from '../components/BottomSheet';
+import BottomSheetScreen from '../components/BottomSheetScreen';
+import { useAppDispatch, useAppSelector } from '../hooks/redux';
+import { startCoopCloseTimestampSelector } from '../store/reselect/user';
+import { clearCoopCloseTimer } from '../store/slices/user';
+import { Display } from '../styles/text';
+import { closeAllChannels } from '../utils/lightning';
+import { showToast } from '../utils/notifications';
+import { useSheetRef } from './SheetRefsProvider';
+
+const imageSrc = require('../assets/illustrations/exclamation-mark.png');
const RETRY_INTERVAL = 1000 * 60 * 5;
const GIVE_UP = 1000 * 60 * 30;
const ForceTransfer = (): ReactElement => {
const { t } = useTranslation('lightning');
- const snapPoints = useSnapPoints('large');
const dispatch = useAppDispatch();
+ const sheetRef = useSheetRef('forceTransfer');
const startTime = useAppSelector(startCoopCloseTimestampSelector);
const [isPending, setIsPending] = useState(false);
- useBottomSheetBackPress('forceTransfer');
-
// try to cooperatively close the channel(s) for 30min
+ // biome-ignore lint/correctness/useExhaustiveDependencies: sheetRef doesn't change
useEffect(() => {
// biome-ignore lint/style/useConst: false alarm
let interval: NodeJS.Timer;
@@ -63,7 +57,7 @@ const ForceTransfer = (): ReactElement => {
console.log('giving up on coop close.');
dispatch(clearCoopCloseTimer());
clearInterval(interval);
- showBottomSheet('forceTransfer');
+ sheetRef.current?.present();
return;
}
@@ -76,7 +70,7 @@ const ForceTransfer = (): ReactElement => {
}, [startTime, dispatch]);
const onCancel = (): void => {
- dispatch(closeSheet('forceTransfer'));
+ sheetRef.current?.close();
};
const onContinue = async (): Promise => {
@@ -102,7 +96,7 @@ const ForceTransfer = (): ReactElement => {
title: t('force_init_title'),
description: t('force_init_msg'),
});
- dispatch(closeSheet('forceTransfer'));
+ sheetRef.current?.close();
} else {
showToast({
type: 'warning',
@@ -114,7 +108,7 @@ const ForceTransfer = (): ReactElement => {
};
return (
-
+
{
onContinue={onContinue}
onCancel={onCancel}
/>
-
+
);
};
diff --git a/src/screens/Settings/PIN/ForgotPIN.tsx b/src/sheets/ForgotPIN.tsx
similarity index 53%
rename from src/screens/Settings/PIN/ForgotPIN.tsx
rename to src/sheets/ForgotPIN.tsx
index eb5534a67..06f90fe28 100644
--- a/src/screens/Settings/PIN/ForgotPIN.tsx
+++ b/src/sheets/ForgotPIN.tsx
@@ -2,43 +2,29 @@ import React, { memo, ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import { Image, StyleSheet, View } from 'react-native';
-import BottomSheetNavigationHeader from '../../../components/BottomSheetNavigationHeader';
-import BottomSheetWrapper from '../../../components/BottomSheetWrapper';
-import SafeAreaInset from '../../../components/SafeAreaInset';
-import Button from '../../../components/buttons/Button';
-import {
- useBottomSheetBackPress,
- useSnapPoints,
-} from '../../../hooks/bottomSheet';
-import { useAppDispatch, useAppSelector } from '../../../hooks/redux';
-import { viewControllerSelector } from '../../../store/reselect/ui';
-import { closeSheet } from '../../../store/slices/ui';
-import { wipeApp } from '../../../store/utils/settings';
-import { BodyM } from '../../../styles/text';
+import BottomSheet from '../components/BottomSheet';
+import BottomSheetNavigationHeader from '../components/BottomSheetNavigationHeader';
+import SafeAreaInset from '../components/SafeAreaInset';
+import Button from '../components/buttons/Button';
+import { wipeApp } from '../store/utils/settings';
+import { BodyM } from '../styles/text';
+import { useSheetRef } from './SheetRefsProvider';
-const imageSrc = require('../../../assets/illustrations/restore.png');
+const imageSrc = require('../assets/illustrations/restore.png');
+
+const sheetId = 'forgotPin';
const ForgotPIN = (): ReactElement => {
const { t } = useTranslation('security');
- const snapPoints = useSnapPoints('large');
- const dispatch = useAppDispatch();
- const { isMounted } = useAppSelector((state) => {
- return viewControllerSelector(state, 'forgotPIN');
- });
-
- useBottomSheetBackPress('forgotPIN');
+ const sheetRef = useSheetRef(sheetId);
const handlePress = (): void => {
wipeApp();
- dispatch(closeSheet('forgotPIN'));
+ sheetRef.current?.close();
};
- if (!isMounted) {
- return <>>;
- }
-
return (
-
+
{
-
+
);
};
diff --git a/src/sheets/HighBalanceWarning.tsx b/src/sheets/HighBalanceWarning.tsx
new file mode 100644
index 000000000..80c5c2ccd
--- /dev/null
+++ b/src/sheets/HighBalanceWarning.tsx
@@ -0,0 +1,124 @@
+import React, { memo, ReactElement, useEffect } from 'react';
+import { Trans, useTranslation } from 'react-i18next';
+
+import BottomSheet from '../components/BottomSheet';
+import BottomSheetScreen from '../components/BottomSheetScreen';
+import { __E2E__ } from '../constants/env';
+import { useAppDispatch, useAppSelector } from '../hooks/redux';
+import { useBalance } from '../hooks/wallet';
+import {
+ ignoreHighBalanceCountSelector,
+ ignoreHighBalanceTimestampSelector,
+} from '../store/reselect/user';
+import { exchangeRatesSelector } from '../store/reselect/wallet';
+import { MAX_WARNINGS, ignoreHighBalance } from '../store/slices/user';
+import { BodyMB, Display } from '../styles/text';
+import { getFiatDisplayValues } from '../utils/displayValues';
+import { openURL } from '../utils/helpers';
+import { useAllSheetRefs, useSheetRef } from './SheetRefsProvider';
+
+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 = 2500; // how long user needs to stay on the home screen before he will see this prompt
+
+const sheetId = 'highBalance';
+
+const HighBalanceWarning = (): ReactElement => {
+ const { t } = useTranslation('other');
+ const { totalBalance } = useBalance();
+ const dispatch = useAppDispatch();
+ const sheetRefs = useAllSheetRefs();
+ const sheetRef = useSheetRef(sheetId);
+ const count = useAppSelector(ignoreHighBalanceCountSelector);
+ const exchangeRates = useAppSelector(exchangeRatesSelector);
+ const ignoreTimestamp = useAppSelector(ignoreHighBalanceTimestampSelector);
+
+ const { fiatValue } = getFiatDisplayValues({
+ satoshis: totalBalance,
+ currency: 'USD',
+ exchangeRates,
+ });
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies: sheetRefs don't change
+ useEffect(() => {
+ // 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 home screen for CHECK_DELAY
+ const shouldShow = () => {
+ const isTimeoutOver = Number(new Date()) - ignoreTimestamp > ASK_INTERVAL;
+ const isAnySheetOpen = sheetRefs.some(({ ref }) => ref.current?.isOpen());
+ const belowMaxWarnings = count < MAX_WARNINGS;
+ const thresholdReached =
+ // fallback in case exchange rates are not available
+ fiatValue !== 0
+ ? fiatValue > BALANCE_THRESHOLD_USD
+ : totalBalance > BALANCE_THRESHOLD_SATS;
+
+ return (
+ !__E2E__ &&
+ !isAnySheetOpen &&
+ isTimeoutOver &&
+ thresholdReached &&
+ belowMaxWarnings
+ );
+ };
+
+ const timer = setTimeout(() => {
+ if (shouldShow()) {
+ sheetRef.current?.present();
+ }
+ }, CHECK_DELAY);
+
+ return () => clearTimeout(timer);
+ }, [ignoreTimestamp, fiatValue, totalBalance, count]);
+
+ const onMore = (): void => {
+ openURL('https://en.bitcoin.it/wiki/Storing_bitcoins');
+ };
+
+ const onDismiss = (): void => {
+ dispatch(ignoreHighBalance(true));
+ sheetRef.current?.close();
+ };
+
+ 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/sheets/LNURLWithdrawNavigation.tsx b/src/sheets/LNURLWithdrawNavigation.tsx
new file mode 100644
index 000000000..fcdcadcff
--- /dev/null
+++ b/src/sheets/LNURLWithdrawNavigation.tsx
@@ -0,0 +1,68 @@
+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 BottomSheet from '../components/BottomSheet';
+import { __E2E__ } from '../constants/env';
+import Amount from '../screens/Wallets/LNURLWithdraw/Amount';
+import Confirm from '../screens/Wallets/LNURLWithdraw/Confirm';
+import { SheetsParamList } from '../store/types/ui';
+import BottomSheetNavigationContainer from './BottomSheetNavigationContainer';
+
+export type LNURLWithdrawNavigationProp =
+ NativeStackNavigationProp;
+
+export type LNURLWithdrawStackParamList = {
+ Amount: { params: LNURLWithdrawParams };
+ Confirm: { amount: number; params: LNURLWithdrawParams };
+};
+
+const Stack = createNativeStackNavigator();
+
+const screenOptions: NativeStackNavigationOptions = {
+ headerShown: false,
+ animation: __E2E__ ? 'none' : 'default',
+};
+
+const LNURLWithdrawNavigation = (): ReactElement => {
+ return (
+
+ {({ data }: { data: SheetsParamList['lnurlWithdraw'] }) => {
+ // if max === min withdrawable amount, skip the Amount screen
+ const initialRouteName =
+ data.minWithdrawable === data.maxWithdrawable ? 'Confirm' : 'Amount';
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ }}
+
+ );
+};
+
+export default memo(LNURLWithdrawNavigation);
diff --git a/src/navigation/bottom-sheet/OrangeTicketNavigation.tsx b/src/sheets/OrangeTicketNavigation.tsx
similarity index 67%
rename from src/navigation/bottom-sheet/OrangeTicketNavigation.tsx
rename to src/sheets/OrangeTicketNavigation.tsx
index f429a8927..4a5c3a4a8 100644
--- a/src/navigation/bottom-sheet/OrangeTicketNavigation.tsx
+++ b/src/sheets/OrangeTicketNavigation.tsx
@@ -13,19 +13,15 @@ import React, {
useState,
} from 'react';
-import BottomSheetWrapper from '../../components/BottomSheetWrapper';
-import { __TREASURE_HUNT_HOST__ } from '../../constants/env';
-import {
- useBottomSheetBackPress,
- 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 BottomSheet from '../components/BottomSheet';
+import { __TREASURE_HUNT_HOST__ } from '../constants/env';
+import { useAppSelector } from '../hooks/redux';
+import ErrorScreen from '../screens/OrangeTicket/Error';
+import Prize from '../screens/OrangeTicket/Prize';
+import UsedCard from '../screens/OrangeTicket/UsedCard';
+import { SheetsParamList } from '../store/types/ui';
+import { getNodeId, waitForLdk } from '../utils/lightning';
+import { showToast } from '../utils/notifications';
import BottomSheetNavigationContainer from './BottomSheetNavigationContainer';
export type OrangeTicketNavigationProp =
@@ -44,19 +40,19 @@ const screenOptions: NativeStackNavigationOptions = {
headerShown: false,
};
-const OrangeTicket = (): ReactElement => {
- const snapPoints = useSnapPoints('large');
+const SheetContent = ({
+ data,
+}: {
+ data: SheetsParamList['orangeTicket'];
+}): ReactElement => {
const [isLoading, setIsLoading] = useState(true);
const [amount, setAmount] = useState();
const [errorCode, setErrorCode] = useState();
const orangeTickets = useAppSelector((state) => state.settings.orangeTickets);
const [initialScreen, setInitialScreen] =
useState('Prize');
- const { isOpen, ticketId } = useAppSelector((state) => {
- return viewControllerSelector(state, 'orangeTicket');
- });
- useBottomSheetBackPress('orangeTicket');
+ const { ticketId } = data;
// biome-ignore lint/correctness/useExhaustiveDependencies: only when ticketId changes
const getPrize = useCallback(async (): Promise => {
@@ -141,45 +137,47 @@ const OrangeTicket = (): ReactElement => {
}, [ticketId]);
useEffect(() => {
- if (!isOpen) {
- setInitialScreen('Prize');
- setIsLoading(true);
- return;
- }
-
getPrize();
- }, [isOpen, getPrize]);
+ }, [getPrize]);
if (isLoading) {
return <>>;
}
return (
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const OrangeTicket = (): ReactElement => {
+ return (
+
+ {({ data }: { data: SheetsParamList['orangeTicket'] }) => {
+ return ;
+ }}
+
);
};
diff --git a/src/sheets/PINNavigation.tsx b/src/sheets/PINNavigation.tsx
new file mode 100644
index 000000000..cbdff0708
--- /dev/null
+++ b/src/sheets/PINNavigation.tsx
@@ -0,0 +1,65 @@
+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 BottomSheet from '../components/BottomSheet';
+import { __E2E__ } from '../constants/env';
+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 { SheetsParamList } from '../store/types/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 => {
+ return (
+
+ {({ data }: { data: SheetsParamList['pinNavigation'] }) => {
+ const showLaterButton = data?.showLaterButton ?? true;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }}
+
+ );
+};
+
+export default memo(PINNavigation);
diff --git a/src/navigation/bottom-sheet/ProfileLinkNavigation.tsx b/src/sheets/ProfileLinkNavigation.tsx
similarity index 61%
rename from src/navigation/bottom-sheet/ProfileLinkNavigation.tsx
rename to src/sheets/ProfileLinkNavigation.tsx
index 2f9b94296..04e3a22ca 100644
--- a/src/navigation/bottom-sheet/ProfileLinkNavigation.tsx
+++ b/src/sheets/ProfileLinkNavigation.tsx
@@ -6,13 +6,10 @@ import {
} 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 { useAppSelector } from '../../hooks/redux';
-import ProfileLink from '../../screens/Profile/ProfileLink';
-import ProfileLinkSuggestions from '../../screens/Profile/ProfileLinkSuggestions';
-import { viewControllerIsOpenSelector } from '../../store/reselect/ui';
+import BottomSheet from '../components/BottomSheet';
+import { __E2E__ } from '../constants/env';
+import ProfileLink from '../screens/Profile/ProfileLink';
+import ProfileLinkSuggestions from '../screens/Profile/ProfileLinkSuggestions';
import BottomSheetNavigationContainer from './BottomSheetNavigationContainer';
export type ProfileLinkNavigationProp =
@@ -31,15 +28,10 @@ const screenOptions: NativeStackNavigationOptions = {
};
const ProfileLinkNavigation = (): ReactElement => {
- const snapPoints = useSnapPoints('small');
- const isOpen = useAppSelector((state) => {
- return viewControllerIsOpenSelector(state, 'profileAddDataForm');
- });
-
return (
-
+
-
+
{
-
+
);
};
diff --git a/src/navigation/bottom-sheet/PubkyAuth.tsx b/src/sheets/PubkyAuth.tsx
similarity index 64%
rename from src/navigation/bottom-sheet/PubkyAuth.tsx
rename to src/sheets/PubkyAuth.tsx
index 9b919ac89..960564221 100644
--- a/src/navigation/bottom-sheet/PubkyAuth.tsx
+++ b/src/sheets/PubkyAuth.tsx
@@ -1,31 +1,26 @@
+import { auth, parseAuthUrl } from '@synonymdev/react-native-pubky';
import React, {
memo,
ReactElement,
useCallback,
useEffect,
useMemo,
+ useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, View } from 'react-native';
-
-import { auth, parseAuthUrl } from '@synonymdev/react-native-pubky';
import Animated, { FadeIn } from 'react-native-reanimated';
-import BottomSheetNavigationHeader from '../../components/BottomSheetNavigationHeader';
-import BottomSheetWrapper from '../../components/BottomSheetWrapper';
-import SafeAreaInset from '../../components/SafeAreaInset';
-import Button from '../../components/buttons/Button';
-import {
- useBottomSheetBackPress,
- useSnapPoints,
-} from '../../hooks/bottomSheet';
-import { useAppSelector } from '../../hooks/redux';
-import { dispatch } from '../../store/helpers.ts';
-import { viewControllerSelector } from '../../store/reselect/ui.ts';
-import { closeSheet } from '../../store/slices/ui.ts';
-import { CheckCircleIcon } from '../../styles/icons.ts';
-import { BodyM, CaptionB, Text13UP, Title } from '../../styles/text';
-import { showToast } from '../../utils/notifications.ts';
-import { getPubkySecretKey } from '../../utils/pubky';
+
+import BottomSheet from '../components/BottomSheet';
+import BottomSheetNavigationHeader from '../components/BottomSheetNavigationHeader';
+import SafeAreaInset from '../components/SafeAreaInset';
+import Button from '../components/buttons/Button';
+import { SheetsParamList } from '../store/types/ui';
+import { CheckCircleIcon } from '../styles/icons';
+import { BodyM, CaptionB, Text13UP, Title } from '../styles/text';
+import { showToast } from '../utils/notifications';
+import { getPubkySecretKey } from '../utils/pubky';
+import { useSheetRef } from './SheetRefsProvider';
const defaultParsedUrl: PubkyAuthDetails = {
relay: '',
@@ -81,18 +76,16 @@ const Permission = memo(
},
);
-const PubkyAuth = (): ReactElement => {
+const SheetContent = ({
+ data,
+}: { data: SheetsParamList['pubkyAuth'] }): ReactElement => {
const { t } = useTranslation('security');
- const snapPoints = useSnapPoints('medium');
- const { url = '' } = useAppSelector((state) => {
- return viewControllerSelector(state, 'pubkyAuth');
- });
- const [parsed, setParsed] =
- React.useState(defaultParsedUrl);
- const [authorizing, setAuthorizing] = React.useState(false);
- const [authSuccess, setAuthSuccess] = React.useState(false);
+ const sheetRef = useSheetRef('pubkyAuth');
+ const [parsed, setParsed] = useState(defaultParsedUrl);
+ const [authorizing, setAuthorizing] = useState(false);
+ const [authSuccess, setAuthSuccess] = useState(false);
- useBottomSheetBackPress('pubkyAuth');
+ const { url } = data;
useEffect(() => {
const fetchParsed = async (): Promise => {
@@ -150,13 +143,7 @@ const PubkyAuth = (): ReactElement => {
[t, url],
);
- const onClose = useMemo(
- () => (): void => {
- dispatch(closeSheet('pubkyAuth'));
- },
- [],
- );
-
+ // biome-ignore lint/correctness/useExhaustiveDependencies: sheetRef doesn't change
const Buttons = useCallback(() => {
if (authSuccess) {
return (
@@ -164,7 +151,7 @@ const PubkyAuth = (): ReactElement => {
style={styles.authorizeButton}
text={t('authorization.success')}
size="large"
- onPress={onClose}
+ onPress={() => sheetRef.current?.close()}
/>
);
}
@@ -174,7 +161,7 @@ const PubkyAuth = (): ReactElement => {
style={styles.closeButton}
text={t('authorization.deny')}
size="large"
- onPress={onClose}
+ onPress={() => sheetRef.current?.close()}
/>
{
/>
>
);
- }, [authSuccess, authorizing, onAuthorize, onClose, t]);
+ }, [authSuccess, authorizing, onAuthorize, t]);
const SuccessCircle = useCallback(() => {
if (authSuccess) {
@@ -203,39 +190,47 @@ const PubkyAuth = (): ReactElement => {
}, [authSuccess]);
return (
-
-
-
- {t('authorization.claims')}
- {parsed.relay}
+
+
+ {t('authorization.claims')}
+ {parsed.relay}
-
+
- {t('authorization.description')}
+ {t('authorization.description')}
-
+
-
- {t('authorization.requested_permissions')}
-
- {parsed.capabilities.map((capability) => {
- return (
-
- );
- })}
+
+ {t('authorization.requested_permissions')}
+
+ {parsed.capabilities.map((capability) => {
+ return (
+
+ );
+ })}
-
+
- {SuccessCircle()}
+ {SuccessCircle()}
- {Buttons()}
-
-
-
+ {Buttons()}
+
+
+ );
+};
+
+const PubkyAuth = (): ReactElement => {
+ return (
+
+ {({ data }: { data: SheetsParamList['pubkyAuth'] }) => {
+ return ;
+ }}
+
);
};
diff --git a/src/sheets/QuickPayPrompt.tsx b/src/sheets/QuickPayPrompt.tsx
new file mode 100644
index 000000000..7adb11798
--- /dev/null
+++ b/src/sheets/QuickPayPrompt.tsx
@@ -0,0 +1,101 @@
+import { useNavigation } from '@react-navigation/native';
+import React, { memo, ReactElement, useEffect } from 'react';
+import { Trans, useTranslation } from 'react-i18next';
+
+import BottomSheet from '../components/BottomSheet';
+import BottomSheetScreen from '../components/BottomSheetScreen';
+import { __E2E__ } from '../constants/env';
+import { useAppDispatch, useAppSelector } from '../hooks/redux';
+import { useBalance } from '../hooks/wallet';
+import { RootNavigationProp } from '../navigation/types';
+import { quickpayIntroSeenSelector } from '../store/reselect/settings';
+import { updateSettings } from '../store/slices/settings';
+import { BodyMB, Display } from '../styles/text';
+import { useAllSheetRefs, useSheetRef } from './SheetRefsProvider';
+
+const imageSrc = require('../assets/illustrations/fast-forward.png');
+
+const CHECK_DELAY = 2500; // how long user needs to stay on the home screen before he will see this prompt
+
+const sheetId = 'quickPay';
+
+const QuickPayPrompt = (): ReactElement => {
+ const { t } = useTranslation('settings');
+ const navigation = useNavigation();
+ const { spendingBalance } = useBalance();
+ const dispatch = useAppDispatch();
+ const sheetRefs = useAllSheetRefs();
+ const sheetRef = useSheetRef(sheetId);
+ const quickpayIntroSeen = useAppSelector(quickpayIntroSeenSelector);
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies: sheetRefs don't change
+ useEffect(() => {
+ // if user hasn't seen this prompt
+ // and has a spending balance
+ // and no other bottom-sheets are shown
+ // and user on home screen for CHECK_DELAY
+ const shouldShow = () => {
+ const isAnySheetOpen = sheetRefs.some(({ ref }) => ref.current?.isOpen());
+ const hasSpendingBalance = spendingBalance > 0;
+
+ return (
+ !__E2E__ && !isAnySheetOpen && !quickpayIntroSeen && hasSpendingBalance
+ );
+ };
+
+ const timer = setTimeout(() => {
+ if (shouldShow()) {
+ sheetRef.current?.present();
+ }
+ }, CHECK_DELAY);
+
+ return () => clearTimeout(timer);
+ }, [quickpayIntroSeen, spendingBalance]);
+
+ const onMore = (): void => {
+ navigation.navigate('Settings', { screen: 'QuickpaySettings' });
+ dispatch(updateSettings({ quickpayIntroSeen: true }));
+ sheetRef.current?.close();
+ };
+
+ const onDismiss = (): void => {
+ dispatch(updateSettings({ quickpayIntroSeen: true }));
+ sheetRef.current?.close();
+ };
+
+ 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/sheets/ReceiveNavigation.tsx b/src/sheets/ReceiveNavigation.tsx
new file mode 100644
index 000000000..46b412822
--- /dev/null
+++ b/src/sheets/ReceiveNavigation.tsx
@@ -0,0 +1,98 @@
+import { NavigationIndependentTree } from '@react-navigation/native';
+import {
+ NativeStackNavigationOptions,
+ NativeStackNavigationProp,
+ createNativeStackNavigator,
+} from '@react-navigation/native-stack';
+import React, { ReactElement, memo } from 'react';
+
+import BottomSheet from '../components/BottomSheet';
+import { __E2E__ } from '../constants/env';
+import { useAppDispatch } 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 { resetInvoice } from '../store/slices/receive';
+import { SheetsParamList } from '../store/types/ui';
+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 dispatch = useAppDispatch();
+
+ const reset = (): void => {
+ dispatch(resetInvoice());
+ };
+
+ return (
+
+ {({ data }: { data: SheetsParamList['receive'] }) => {
+ const initialRouteName = data?.screen ?? 'ReceiveQR';
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }}
+
+ );
+};
+
+export default memo(ReceiveNavigation);
diff --git a/src/sheets/ReceivedTransaction.tsx b/src/sheets/ReceivedTransaction.tsx
new file mode 100644
index 000000000..9392a45af
--- /dev/null
+++ b/src/sheets/ReceivedTransaction.tsx
@@ -0,0 +1,166 @@
+import Lottie from 'lottie-react-native';
+import React, { memo, ReactElement, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Image, StyleSheet, View } from 'react-native';
+import { useReducedMotion } from 'react-native-reanimated';
+
+import AmountToggle from '../components/AmountToggle';
+import BottomSheet from '../components/BottomSheet';
+import BottomSheetNavigationHeader from '../components/BottomSheetNavigationHeader';
+import SafeAreaInset from '../components/SafeAreaInset';
+import Button from '../components/buttons/Button';
+import { __E2E__ } from '../constants/env';
+import { rootNavigation } from '../navigation/root/RootNavigationContainer';
+import { EActivityType } from '../store/types/activity';
+import { SheetsParamList } from '../store/types/ui';
+import { getRandomOkText } from '../utils/i18n/helpers';
+import { useSheetRef } from './SheetRefsProvider';
+
+const confettiOrangeSrc = require('../assets/lottie/confetti-orange.json');
+const confettiPurpleSrc = require('../assets/lottie/confetti-purple.json');
+const imageSrc = require('../assets/illustrations/coin-stack-x.png');
+
+const SheetContent = ({
+ data,
+}: { data: SheetsParamList['receivedTx'] }): ReactElement => {
+ const reducedMotion = useReducedMotion();
+ const { t } = useTranslation('wallet');
+ const sheetRef = useSheetRef('receivedTx');
+
+ const { id, activityType, value } = data;
+ const isOnchainItem = activityType === EActivityType.onchain;
+
+ const buttonText = useMemo(() => getRandomOkText(), []);
+
+ const onButtonPress = (): void => {
+ sheetRef.current?.close();
+ };
+
+ const onAmountPress = (): void => {
+ sheetRef.current?.close();
+ rootNavigation.navigate('ActivityDetail', { id });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const ReceivedTransaction = (): ReactElement => {
+ return (
+
+ {({ data }: { data: SheetsParamList['receivedTx'] }) => {
+ return ;
+ }}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ root: {
+ flex: 1,
+ },
+ confetti: {
+ ...StyleSheet.absoluteFillObject,
+ zIndex: 0,
+ },
+ lottie: {
+ height: '100%',
+ },
+ content: {
+ flex: 1,
+ paddingHorizontal: 16,
+ },
+ imageContainer: {
+ marginTop: 'auto',
+ justifyContent: 'center',
+ alignItems: 'center',
+ alignSelf: 'center',
+ height: 250,
+ width: 200,
+ },
+ image1: {
+ width: 220,
+ height: 220,
+ position: 'absolute',
+ bottom: '14%',
+ transform: [{ scaleX: -1 }, { rotate: '-10deg' }],
+ zIndex: 1,
+ },
+ image2: {
+ width: 220,
+ height: 220,
+ position: 'absolute',
+ bottom: '-17%',
+ transform: [{ scaleX: -1 }],
+ },
+ image3: {
+ width: 220,
+ height: 220,
+ position: 'absolute',
+ bottom: '12%',
+ left: '12%',
+ transform: [{ scaleX: 1 }, { rotate: '210deg' }],
+ zIndex: 2,
+ },
+ image4: {
+ width: 220,
+ height: 220,
+ position: 'absolute',
+ bottom: '75%',
+ left: '60%',
+ transform: [{ rotate: '30deg' }],
+ },
+ buttonContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ zIndex: 1,
+ },
+ button: {
+ flex: 1,
+ },
+});
+
+export default memo(ReceivedTransaction);
diff --git a/src/sheets/SendNavigation.tsx b/src/sheets/SendNavigation.tsx
new file mode 100644
index 000000000..45c9853fd
--- /dev/null
+++ b/src/sheets/SendNavigation.tsx
@@ -0,0 +1,210 @@
+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 BottomSheet from '../components/BottomSheet';
+import { __E2E__ } from '../constants/env';
+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 {
+ selectedNetworkSelector,
+ selectedWalletSelector,
+ transactionSelector,
+} from '../store/reselect/wallet';
+import { EActivityType } from '../store/types/activity';
+import { SheetsParamList } from '../store/types/ui';
+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);
+ }
+ },
+};
+
+// This is a helper function to get type-safe params for a given screen.
+const getScreenParams = (
+ data: SheetsParamList['send'] | undefined,
+ expectedScreen: NonNullable['screen'],
+): T | undefined => {
+ if (data?.screen === expectedScreen) {
+ return data as T;
+ }
+ return undefined;
+};
+
+const SendNavigation = (): ReactElement => {
+ const lightningBalance = useLightningBalance(false);
+ const selectedWallet = useAppSelector(selectedWalletSelector);
+ const selectedNetwork = useAppSelector(selectedNetworkSelector);
+ const transaction = useAppSelector(transactionSelector);
+
+ 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 (
+
+ {({ data }: { data: SheetsParamList['send'] }) => {
+ const initialRouteName = data?.screen ?? 'Recipient';
+
+ const quickpayParams = getScreenParams<{
+ screen: 'Quickpay';
+ invoice: string;
+ amount: number;
+ }>(data, 'Quickpay');
+
+ const lnurlAmountParams = getScreenParams<{
+ screen: 'LNURLAmount';
+ pParams: LNURLPayParams;
+ url: string;
+ }>(data, 'LNURLAmount');
+
+ const lnurlConfirmParams = getScreenParams<{
+ screen: 'LNURLConfirm';
+ pParams: LNURLPayParams;
+ url: string;
+ amount?: number;
+ }>(data, 'LNURLConfirm');
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }}
+
+ );
+};
+
+export default memo(SendNavigation);
diff --git a/src/sheets/SheetRefsProvider.tsx b/src/sheets/SheetRefsProvider.tsx
new file mode 100644
index 000000000..7852d24ba
--- /dev/null
+++ b/src/sheets/SheetRefsProvider.tsx
@@ -0,0 +1,47 @@
+import { BottomSheetModal } from '@gorhom/bottom-sheet';
+import { ReactNode, RefObject, createContext, useContext } from 'react';
+import { SheetId, sheetIds } from '../store/types/ui';
+
+const sheetRefsMap = new Map>();
+
+const SheetRefsContext = createContext(sheetRefsMap);
+
+export const SheetRefsProvider: React.FC<{ children: ReactNode }> = ({
+ children,
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export const useSheetRef = (id: SheetId) => {
+ const refsMap = useContext(SheetRefsContext);
+
+ if (!refsMap.has(id)) {
+ refsMap.set(id, { current: null });
+ }
+
+ return refsMap.get(id)!;
+};
+
+export const useAllSheetRefs = () => {
+ const refsMap = useContext(SheetRefsContext);
+
+ // Ensure all possible sheet refs are registered
+ sheetIds.forEach((id) => {
+ if (!refsMap.has(id)) {
+ refsMap.set(id, { current: null });
+ }
+ });
+
+ return Array.from(refsMap.entries()).map(([id, ref]) => ({ id, ref }));
+};
+
+export const getSheetRefOutsideComponent = (key: SheetId) => {
+ if (!sheetRefsMap.has(key)) {
+ sheetRefsMap.set(key, { current: null });
+ }
+ return sheetRefsMap.get(key)!;
+};
diff --git a/src/screens/Activity/TagsPrompt.tsx b/src/sheets/Tags.tsx
similarity index 59%
rename from src/screens/Activity/TagsPrompt.tsx
rename to src/sheets/Tags.tsx
index d6106e2a6..a7abedfb1 100644
--- a/src/screens/Activity/TagsPrompt.tsx
+++ b/src/sheets/Tags.tsx
@@ -2,19 +2,14 @@ import React, { memo, ReactElement } 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 {
- useBottomSheetBackPress,
- useSnapPoints,
-} from '../../hooks/bottomSheet';
-import { useAppDispatch, useAppSelector } from '../../hooks/redux';
-import { lastUsedTagsSelector } from '../../store/reselect/metadata';
-import { closeSheet } from '../../store/slices/ui';
-import { BodyS, Subtitle, Text13UP } from '../../styles/text';
+import BottomSheet from '../components/BottomSheet';
+import SafeAreaInset from '../components/SafeAreaInset';
+import Tag from '../components/Tag';
+import { useAppSelector } from '../hooks/redux';
+import { lastUsedTagsSelector } from '../store/reselect/metadata';
+import { BodyS, Subtitle, Text13UP } from '../styles/text';
-const TagsPrompt = ({
+const TagsSheet = ({
tags,
onAddTag,
}: {
@@ -22,22 +17,11 @@ const TagsPrompt = ({
onAddTag: (tag: string) => void;
}): ReactElement => {
const { t } = useTranslation('wallet');
- const snapPoints = useSnapPoints('medium');
- const dispatch = useAppDispatch();
const lastUsed = useAppSelector(lastUsedTagsSelector);
const suggestions = lastUsed.filter((tg) => !tags.includes(tg));
- useBottomSheetBackPress('tagsPrompt');
-
- const handleClose = (): void => {
- dispatch(closeSheet('tagsPrompt'));
- };
-
return (
-
+
{t('tags_filter_title')}
@@ -59,7 +43,7 @@ const TagsPrompt = ({
-
+
);
};
@@ -87,4 +71,4 @@ const styles = StyleSheet.create({
},
});
-export default memo(TagsPrompt);
+export default memo(TagsSheet);
diff --git a/src/navigation/bottom-sheet/TransferFailed.tsx b/src/sheets/TransferFailed.tsx
similarity index 65%
rename from src/navigation/bottom-sheet/TransferFailed.tsx
rename to src/sheets/TransferFailed.tsx
index 4aa3e24f4..deeecb931 100644
--- a/src/navigation/bottom-sheet/TransferFailed.tsx
+++ b/src/sheets/TransferFailed.tsx
@@ -1,28 +1,23 @@
+// NOTE: currently not used
+
import React, { memo, ReactElement } from 'react';
import { Trans, useTranslation } from 'react-i18next';
-import BottomSheetScreen from '../../components/BottomSheetScreen';
-import BottomSheetWrapper from '../../components/BottomSheetWrapper';
-import {
- useBottomSheetBackPress,
- useSnapPoints,
-} from '../../hooks/bottomSheet';
-import { Display } from '../../styles/text';
+import BottomSheet from '../components/BottomSheet';
+import BottomSheetScreen from '../components/BottomSheetScreen';
+import { Display } from '../styles/text';
const imageSrc = require('../../assets/illustrations/cross.png');
const TransferFailed = (): ReactElement => {
const { t } = useTranslation('lightning');
- const snapPoints = useSnapPoints('large');
-
- useBottomSheetBackPress('transferFailed');
const onCancel = (): void => {};
const onContinue = async (): Promise => {};
return (
-
+
{
onContinue={onContinue}
onCancel={onCancel}
/>
-
+
);
};
diff --git a/src/navigation/bottom-sheet/TreasureHuntNavigation.tsx b/src/sheets/TreasureHuntNavigation.tsx
similarity index 57%
rename from src/navigation/bottom-sheet/TreasureHuntNavigation.tsx
rename to src/sheets/TreasureHuntNavigation.tsx
index 00c14ce48..5c77b8b4c 100644
--- a/src/navigation/bottom-sheet/TreasureHuntNavigation.tsx
+++ b/src/sheets/TreasureHuntNavigation.tsx
@@ -12,25 +12,24 @@ import React, {
useState,
} from 'react';
-import BottomSheetWrapper from '../../components/BottomSheetWrapper';
-import { __E2E__ } from '../../constants/env';
-import { __TREASURE_HUNT_HOST__ } from '../../constants/env';
-import { useSnapPoints } from '../../hooks/bottomSheet';
-import { useAppDispatch, useAppSelector } from '../../hooks/redux';
-import Airdrop from '../../screens/TreasureHunt/Airdrop';
-import Chest from '../../screens/TreasureHunt/Chest';
-import ErrorScreen from '../../screens/TreasureHunt/Error';
-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 BottomSheet from '../components/BottomSheet';
+import { __E2E__ } from '../constants/env';
+import { __TREASURE_HUNT_HOST__ } from '../constants/env';
+import { useAppDispatch, useAppSelector } from '../hooks/redux';
+import Airdrop from '../screens/TreasureHunt/Airdrop';
+import Chest from '../screens/TreasureHunt/Chest';
+import ErrorScreen from '../screens/TreasureHunt/Error';
+import Loading from '../screens/TreasureHunt/Loading';
+import Prize from '../screens/TreasureHunt/Prize';
+import { addTreasureChest } from '../store/slices/settings';
+import { SheetsParamList } from '../store/types/ui';
import BottomSheetNavigationContainer from './BottomSheetNavigationContainer';
export type TreasureHuntNavigationProp =
NativeStackNavigationProp;
export type TreasureHuntStackParamList = {
- Chest: undefined;
+ Chest: { chestId: string };
Loading: { chestId: string };
Prize: { chestId: string };
Airdrop: { chestId: string };
@@ -45,16 +44,16 @@ const screenOptions: NativeStackNavigationOptions = {
animation: __E2E__ ? 'none' : 'default',
};
-const TreasureHuntNavigation = (): ReactElement => {
- const snapPoints = useSnapPoints('large');
+const SheetContent = ({
+ data,
+}: { data: SheetsParamList['treasureHunt'] }): ReactElement => {
const dispatch = useAppDispatch();
const { treasureChests } = useAppSelector((state) => state.settings);
const [isLoading, setIsLoading] = useState(true);
const [initialScreen, setInitialScreen] =
useState('Chest');
- const { isOpen, chestId } = useAppSelector((state) => {
- return viewControllerSelector(state, 'treasureHunt');
- });
+
+ const { chestId } = data;
const found = treasureChests.find((chest) => chest.chestId === chestId!);
@@ -102,11 +101,6 @@ const TreasureHuntNavigation = (): ReactElement => {
// biome-ignore lint/correctness/useExhaustiveDependencies: onOpen
useEffect(() => {
- if (!isOpen) {
- setIsLoading(true);
- return;
- }
-
if (found) {
if (found.isAirdrop) {
setInitialScreen('Airdrop');
@@ -121,40 +115,52 @@ const TreasureHuntNavigation = (): ReactElement => {
} else {
getChest();
}
- }, [isOpen, getChest]);
+ }, [getChest]);
- if (!isOpen || isLoading) {
+ if (isLoading) {
return <>>;
}
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const TreasureHuntNavigation = (): ReactElement => {
+ return (
+
+ {({ data }: { data: SheetsParamList['treasureHunt'] }) => {
+ return ;
+ }}
+
);
};
diff --git a/src/store/reselect/ui.ts b/src/store/reselect/ui.ts
index 6ec6fb34b..01bbe7700 100644
--- a/src/store/reselect/ui.ts
+++ b/src/store/reselect/ui.ts
@@ -2,42 +2,11 @@ import { createSelector } from '@reduxjs/toolkit';
import { RootState } from '..';
import { TBackupItem } from '../types/backup';
import { EBackupCategory } from '../types/backup';
-import {
- IViewControllerData,
- THealthState,
- TProfileLink,
- TSendTransaction,
- TUiViewController,
- TViewController,
-} from '../types/ui';
+import { THealthState, TProfileLink, TSendTransaction } from '../types/ui';
import { backupSelector } from './backup';
import { blocktankPaidOrdersFullSelector } from './blocktank';
import { openChannelsSelector, pendingChannelsSelector } from './lightning';
-export const viewControllersSelector = (
- state: RootState,
-): TUiViewController => {
- return state.ui.viewControllers;
-};
-
-export const viewControllerSelector = (
- state: RootState,
- viewController: TViewController,
-): IViewControllerData => {
- return state.ui.viewControllers[viewController];
-};
-
-export const viewControllerIsOpenSelector = (
- state: RootState,
- viewController: TViewController,
-): boolean => {
- return state.ui.viewControllers[viewController].isOpen;
-};
-
-export const showLaterButtonSelector = (state: RootState): boolean => {
- return state.ui.viewControllers.PINNavigation.showLaterButton ?? false;
-};
-
export const profileLinkSelector = (state: RootState): TProfileLink => {
return state.ui.profileLink;
};
diff --git a/src/store/shapes/ui.ts b/src/store/shapes/ui.ts
index 2499defd4..c29078968 100644
--- a/src/store/shapes/ui.ts
+++ b/src/store/shapes/ui.ts
@@ -2,34 +2,6 @@
import { TUiState } from '../types/ui';
-export const defaultViewController = { isOpen: false, isMounted: false };
-
-export const defaultViewControllers: TUiState['viewControllers'] = {
- activityTagsPrompt: defaultViewController,
- addContactModal: defaultViewController,
- appUpdatePrompt: defaultViewController,
- backupNavigation: defaultViewController,
- backupPrompt: defaultViewController,
- boostPrompt: defaultViewController,
- connectionClosed: defaultViewController,
- forceTransfer: defaultViewController,
- forgotPIN: defaultViewController,
- highBalance: defaultViewController,
- newTxPrompt: defaultViewController,
- orangeTicket: defaultViewController,
- PINNavigation: defaultViewController,
- profileAddDataForm: defaultViewController,
- pubkyAuth: defaultViewController,
- quickPay: defaultViewController,
- receiveNavigation: defaultViewController,
- sendNavigation: defaultViewController,
- timeRangePrompt: defaultViewController,
- transferFailed: defaultViewController,
- treasureHunt: defaultViewController,
- tagsPrompt: defaultViewController,
- lnurlWithdraw: defaultViewController,
-};
-
export const initialUiState: TUiState = {
appState: 'active',
availableUpdate: null,
@@ -40,12 +12,10 @@ export const initialUiState: TUiState = {
isLDKReady: false, // LDK node running and connected
language: 'en',
profileLink: { title: '', url: '' },
- timeZone: 'UTC',
- // Used to control bottom-sheets throughout the app
- viewControllers: defaultViewControllers,
sendTransaction: {
fromAddressViewer: false, // When true, ensures tx inputs are not cleared when sweeping from address viewer.
paymentMethod: 'onchain',
uri: '',
},
+ timeZone: 'UTC',
};
diff --git a/src/store/slices/ui.ts b/src/store/slices/ui.ts
index 318af00c6..f9221b910 100644
--- a/src/store/slices/ui.ts
+++ b/src/store/slices/ui.ts
@@ -7,7 +7,6 @@ import {
TProfileLink,
TSendTransaction,
TUiState,
- ViewControllerParamList,
} from '../types/ui';
export const uiSlice = createSlice({
@@ -20,46 +19,6 @@ export const uiSlice = createSlice({
setAppUpdateInfo: (state, action: PayloadAction) => {
state.availableUpdate = action.payload;
},
- toggleSheet: (
- state,
- action: PayloadAction<{
- view: keyof ViewControllerParamList;
- params: any;
- }>,
- ) => {
- state.viewControllers[action.payload.view] = {
- ...action.payload.params,
- isOpen: !state.viewControllers[action.payload.view].isOpen,
- isMounted: true,
- };
- },
- showSheet: (
- state,
- action: PayloadAction<{
- view: keyof ViewControllerParamList;
- params: any;
- }>,
- ) => {
- state.viewControllers[action.payload.view] = {
- ...action.payload.params,
- isOpen: true,
- isMounted: true,
- };
- },
- closeSheet: (
- state,
- action: PayloadAction,
- ) => {
- state.viewControllers[action.payload] = {
- isOpen: false,
- isMounted: true,
- };
- },
- closeAllSheets: (state) => {
- Object.keys(state.viewControllers).forEach((key) => {
- state.viewControllers[key].isOpen = false;
- });
- },
updateProfileLink: (state, action: PayloadAction) => {
state.profileLink = Object.assign(state.profileLink, action.payload);
},
@@ -81,10 +40,6 @@ const { actions, reducer } = uiSlice;
export const {
updateUi,
setAppUpdateInfo,
- showSheet,
- toggleSheet,
- closeSheet,
- closeAllSheets,
updateProfileLink,
updateSendTransaction,
resetUiState,
diff --git a/src/store/types/ui.ts b/src/store/types/ui.ts
index d48f78e51..7b2c90546 100644
--- a/src/store/types/ui.ts
+++ b/src/store/types/ui.ts
@@ -1,30 +1,57 @@
import { LNURLPayParams, LNURLWithdrawParams } from 'js-lnurl';
import { AppStateStatus } from 'react-native';
-import { ReceiveStackParamList } from '../../navigation/bottom-sheet/ReceiveNavigation';
-import { SendStackParamList } from '../../navigation/bottom-sheet/SendNavigation';
+import { ReceiveStackParamList } from '../../sheets/ReceiveNavigation';
+import { SendStackParamList } from '../../sheets/SendNavigation';
import { EActivityType, TOnchainActivityItem } from './activity';
-export type ViewControllerParamList = {
- activityTagsPrompt: { id: string };
- addContactModal: undefined;
- appUpdatePrompt: undefined;
+// Used to ensure all sheet refs are registered
+export const sheetIds: SheetId[] = [
+ 'activityTags',
+ 'addContact',
+ 'appUpdate',
+ 'backupNavigation',
+ 'backupPrompt',
+ 'boost',
+ 'connectionClosed',
+ 'datePicker',
+ 'forceTransfer',
+ 'forgotPin',
+ 'highBalance',
+ 'lnurlWithdraw',
+ 'orangeTicket',
+ 'pinNavigation',
+ 'profileLink',
+ 'pubkyAuth',
+ 'quickPay',
+ 'receive',
+ 'receivedTx',
+ 'send',
+ 'tags',
+ 'transferFailed',
+ 'treasureHunt',
+];
+
+export type SheetsParamList = {
+ addContact: undefined;
+ activityTags: { id: string };
+ appUpdate: undefined;
backupNavigation: undefined;
backupPrompt: undefined;
- boostPrompt: { onchainActivityItem: TOnchainActivityItem };
+ boost: { activityItem: TOnchainActivityItem };
connectionClosed: undefined;
+ datePicker: undefined;
forceTransfer: undefined;
- forgotPIN: undefined;
+ forgotPin: undefined;
highBalance: undefined;
- newTxPrompt: {
- activityItem: { id: string; activityType: EActivityType; value: number };
- };
+ lnurlWithdraw: LNURLWithdrawParams;
orangeTicket: { ticketId: string };
- PINNavigation: { showLaterButton: boolean };
- profileAddDataForm: undefined;
+ pinNavigation: { showLaterButton: boolean };
+ profileLink: undefined;
pubkyAuth: { url: string };
quickPay: undefined;
- receiveNavigation: { receiveScreen: keyof ReceiveStackParamList } | undefined;
- sendNavigation:
+ receive: { screen: keyof ReceiveStackParamList } | undefined;
+ receivedTx: { id: string; activityType: EActivityType; value: number };
+ send:
| { screen: keyof SendStackParamList }
| { screen: 'Quickpay'; invoice: string; amount: number }
| { screen: 'LNURLAmount'; pParams: LNURLPayParams; url: string }
@@ -35,42 +62,12 @@ export type ViewControllerParamList = {
amount?: number;
}
| undefined;
- timeRangePrompt: undefined;
+ tags: undefined;
transferFailed: undefined;
treasureHunt: { chestId: string };
- tagsPrompt: undefined;
- lnurlWithdraw: { wParams: LNURLWithdrawParams };
-};
-
-export type TViewController = keyof ViewControllerParamList;
-
-type TViewProps = { isOpen: boolean; isMounted: boolean };
-
-export type TUiViewController = {
- [key in TViewController]: undefined extends ViewControllerParamList[key]
- ? TViewProps
- : Partial & TViewProps;
};
-// this type is needed because reselect doesn't offer good parameter typing
-export type IViewControllerData = {
- isOpen: boolean;
- isMounted: boolean;
- activityItem?: { id: string; activityType: EActivityType; value: number };
- chestId?: string;
- onchainActivityItem?: TOnchainActivityItem;
- id?: string;
- screen?: keyof SendStackParamList;
- receiveScreen?: keyof ReceiveStackParamList;
- showLaterButton?: boolean;
- ticketId?: string;
- txId?: string;
- url?: string;
- wParams?: LNURLWithdrawParams;
- pParams?: LNURLPayParams;
- invoice?: string;
- amount?: number;
-};
+export type SheetId = keyof SheetsParamList;
export type TProfileLink = {
title: string;
@@ -105,6 +102,5 @@ export type TUiState = {
language: string;
profileLink: TProfileLink;
timeZone: string;
- viewControllers: TUiViewController;
sendTransaction: TSendTransaction;
};
diff --git a/src/store/utils/activity.ts b/src/store/utils/activity.ts
index 75bae1a68..def06bb80 100644
--- a/src/store/utils/activity.ts
+++ b/src/store/utils/activity.ts
@@ -11,9 +11,8 @@ import { getCurrentWallet } from '../../utils/wallet';
import { dispatch, getBlocktankStore } from '../helpers';
import { addActivityItem, updateActivityItems } from '../slices/activity';
import { updateSettings } from '../slices/settings';
-import { closeSheet } from '../slices/ui';
import { EActivityType, TLightningActivityItem } from '../types/activity';
-import { showBottomSheet } from './ui';
+import { closeSheet, showSheet } from './ui';
/**
* Attempts to determine if a given channel open was in response to
@@ -60,9 +59,13 @@ export const addCJitActivityItem = async (channelId: string): Promise => {
dispatch(addActivityItem(activityItem));
dispatch(updateSettings({ hideOnboardingMessage: true }));
- dispatch(closeSheet('receiveNavigation'));
+ closeSheet('receive');
vibrate({ type: 'default' });
- showBottomSheet('newTxPrompt', { activityItem });
+ showSheet('receivedTx', {
+ id: activityItem.id,
+ activityType: EActivityType.lightning,
+ value: activityItem.value,
+ });
// redux-persist doesn't save to MMKV when the app is backgrounded
// Quickfix: manually flush the store after adding the activity item
diff --git a/src/store/utils/ui.ts b/src/store/utils/ui.ts
index 4c565e790..9ced50477 100644
--- a/src/store/utils/ui.ts
+++ b/src/store/utils/ui.ts
@@ -1,36 +1,32 @@
import { Platform } from 'react-native';
import { getBuildNumber } from 'react-native-device-info';
+import { Keyboard } from '../../hooks/keyboard';
+import { getSheetRefOutsideComponent } from '../../sheets/SheetRefsProvider';
import { vibrate } from '../../utils/helpers';
import { dispatch } from '../helpers';
-import {
- closeSheet,
- setAppUpdateInfo,
- showSheet,
- toggleSheet,
-} from '../slices/ui';
+import { setAppUpdateInfo } from '../slices/ui';
import { EActivityType } from '../types/activity';
-import { TAvailableUpdate, ViewControllerParamList } from '../types/ui';
+import { SheetId, SheetsParamList, TAvailableUpdate } from '../types/ui';
const releaseUrl =
'https://github.com/synonymdev/bitkit/releases/download/updater/release.json';
-export const showBottomSheet = (
- ...args: undefined extends ViewControllerParamList[View]
- ? [view: View] | [view: View, params: ViewControllerParamList[View]]
- : [view: View, params: ViewControllerParamList[View]]
+export const showSheet = (
+ ...args: undefined extends SheetsParamList[Id]
+ ? [id: Id] | [id: Id, params: SheetsParamList[Id]]
+ : [id: Id, params: SheetsParamList[Id]]
): void => {
- const [view, params] = args;
- dispatch(showSheet({ view, params }));
+ const [id, params] = args;
+ const sheetRef = getSheetRefOutsideComponent(id);
+ sheetRef.current?.present(params);
};
-export const toggleBottomSheet = (
- ...args: undefined extends ViewControllerParamList[View]
- ? [view: View] | [view: View, params: ViewControllerParamList[View]]
- : [view: View, params: ViewControllerParamList[View]]
-): void => {
- const [view, params] = args;
- dispatch(toggleSheet({ view, params }));
+export const closeSheet = async (id: SheetId): Promise => {
+ const sheetRef = getSheetRefOutsideComponent(id);
+ await Keyboard.dismiss();
+ // NOTE: params are reset in onClose of BottomSheet
+ sheetRef.current?.close();
};
export const showNewOnchainTxPrompt = ({
@@ -41,14 +37,12 @@ export const showNewOnchainTxPrompt = ({
value: number;
}): void => {
vibrate({ type: 'default' });
- showBottomSheet('newTxPrompt', {
- activityItem: {
- id,
- activityType: EActivityType.onchain,
- value,
- },
+ showSheet('receivedTx', {
+ id,
+ activityType: EActivityType.onchain,
+ value,
});
- dispatch(closeSheet('receiveNavigation'));
+ closeSheet('receive');
};
export const checkForAppUpdate = async (): Promise => {
diff --git a/src/utils/lightning/index.ts b/src/utils/lightning/index.ts
index e987722d0..dcdace8a0 100644
--- a/src/utils/lightning/index.ts
+++ b/src/utils/lightning/index.ts
@@ -41,7 +41,7 @@ import {
__BACKUPS_SERVER_HOST__,
__BACKUPS_SERVER_PUBKEY__,
} from '../../constants/env';
-import { sendNavigation } from '../../navigation/bottom-sheet/SendNavigation';
+import { sendNavigation } from '../../sheets/SendNavigation';
import {
dispatch,
getBlocktankStore,
@@ -52,7 +52,7 @@ import {
import { addActivityItem } from '../../store/slices/activity';
import { initialFeesState } from '../../store/slices/fees';
import { updateBackupState } from '../../store/slices/lightning';
-import { closeSheet, updateUi } from '../../store/slices/ui';
+import { updateUi } from '../../store/slices/ui';
import {
EActivityType,
TLightningActivityItem,
@@ -79,7 +79,7 @@ import {
updateLightningNodeIdThunk,
updateLightningNodeVersionThunk,
} from '../../store/utils/lightning';
-import { showBottomSheet } from '../../store/utils/ui';
+import { closeSheet, showSheet } from '../../store/utils/ui';
import { getBlocktankInfo, isGeoBlocked, logToBlocktank } from '../blocktank';
import {
promiseTimeout,
@@ -574,9 +574,13 @@ export const handleLightningPaymentSubscription = async ({
};
vibrate({ type: 'default' });
- showBottomSheet('newTxPrompt', { activityItem });
- dispatch(closeSheet('receiveNavigation'));
- dispatch(closeSheet('orangeTicket'));
+ showSheet('receivedTx', {
+ id: activityItem.id,
+ activityType: EActivityType.lightning,
+ value: activityItem.value,
+ });
+ closeSheet('receive');
+ closeSheet('orangeTicket');
dispatch(addActivityItem(activityItem));
await refreshLdk({ selectedWallet, selectedNetwork });
@@ -702,7 +706,7 @@ export const subscribeToLightningPayments = ({
await closeChannelThunk(res);
if (res.reason === EChannelClosureReason.CommitmentTxConfirmed) {
// counterparty force closed the channel
- showBottomSheet('connectionClosed');
+ showSheet('connectionClosed');
}
updateSlashPayConfig({ selectedWallet, selectedNetwork });
},
diff --git a/src/utils/scanner/scanner.ts b/src/utils/scanner/scanner.ts
index 064c4f895..f6957442c 100644
--- a/src/utils/scanner/scanner.ts
+++ b/src/utils/scanner/scanner.ts
@@ -11,16 +11,10 @@ import {
validateAddress,
} from 'beignet';
import bip21 from 'bip21';
-import {
- LNURLAuthParams,
- LNURLChannelParams,
- LNURLPayParams,
- LNURLWithdrawParams,
-} from 'js-lnurl';
import URLParse from 'url-parse';
-import { sendNavigation } from '../../navigation/bottom-sheet/SendNavigation';
import { rootNavigation } from '../../navigation/root/RootNavigationContainer';
+import { sendNavigation } from '../../sheets/SendNavigation';
import {
resetSendTransaction,
setupOnChainTransaction,
@@ -31,9 +25,9 @@ import {
getSettingsStore,
getSlashtagsStore,
} from '../../store/helpers';
-import { closeSheet, updateSendTransaction } from '../../store/slices/ui';
+import { updateSendTransaction } from '../../store/slices/ui';
import { EDenomination } from '../../store/types/wallet';
-import { showBottomSheet } from '../../store/utils/ui';
+import { closeSheet, showSheet } from '../../store/utils/ui';
import { fiatToBitcoinUnit } from '../conversion';
import { getBitcoinDisplayValues } from '../displayValues';
import i18n from '../i18n';
@@ -154,7 +148,7 @@ export const processUri = async ({
// Handle
if (data && !validateOnly) {
- await handleData({ data, uri });
+ await handleData({ data, uri, source });
}
return ok('');
@@ -234,25 +228,25 @@ export const parseUri = async (
if (params.tag === 'login') {
return ok({
type: EQRDataType.lnurlAuth,
- lnUrlParams: params as LNURLAuthParams,
+ lnUrlParams: params,
});
}
if (params.tag === 'withdrawRequest') {
return ok({
type: EQRDataType.lnurlWithdraw,
- lnUrlParams: params as LNURLWithdrawParams,
+ lnUrlParams: params,
});
}
if (params.tag === 'channelRequest') {
return ok({
type: EQRDataType.lnurlChannel,
- lnUrlParams: params as LNURLChannelParams,
+ lnUrlParams: params,
});
}
if (params.tag === 'payRequest') {
return ok({
type: EQRDataType.lnurlPay,
- lnUrlParams: params as LNURLPayParams,
+ lnUrlParams: params,
});
}
}
@@ -267,7 +261,7 @@ export const parseUri = async (
const params = res.value;
return ok({
type: EQRDataType.lnurlAddress,
- lnUrlParams: params as LNURLPayParams,
+ lnUrlParams: params,
address: uri,
});
}
@@ -578,9 +572,11 @@ export const processSlashPayUrl = async (
const handleData = async ({
data,
uri,
+ source,
}: {
data: QRData;
uri: string;
+ source: 'mainScanner' | 'send';
}): Promise> => {
const selectedWallet = getSelectedWallet();
const selectedNetwork = getSelectedNetwork();
@@ -588,8 +584,12 @@ const handleData = async ({
switch (type) {
case EQRDataType.slashtag: {
- handleSlashtagURL(data.url);
- dispatch(closeSheet('addContactModal'));
+ const onSuccess = (): void => {
+ closeSheet('addContact');
+ rootNavigation.navigate('ContactEdit', { url: data.url });
+ };
+
+ handleSlashtagURL(data.url, onSuccess);
return ok('');
}
case EQRDataType.slashAuth: {
@@ -619,10 +619,14 @@ const handleData = async ({
if (enableQuickpay && amount && amount < quickpayAmountSats) {
const screen = 'Quickpay';
const params = { invoice: lightningInvoice, amount };
- // If BottomSheet is not open yet (MainScanner)
- showBottomSheet('sendNavigation', { screen, ...params });
- // If BottomSheet is already open (SendScanner)
- sendNavigation.navigate(screen, params);
+
+ if (source === 'mainScanner') {
+ // If BottomSheet is not open yet (MainScanner)
+ showSheet('send', { screen, ...params });
+ } else {
+ // If BottomSheet is already open (SendScanner)
+ sendNavigation.navigate(screen, params);
+ }
return ok('');
}
@@ -636,10 +640,14 @@ const handleData = async ({
dispatch(updateSendTransaction({ paymentMethod, uri }));
const screen = amount ? 'ReviewAndSend' : 'Amount';
- // If BottomSheet is not open yet (MainScanner)
- showBottomSheet('sendNavigation', { screen });
- // If BottomSheet is already open (SendScanner)
- sendNavigation.navigate(screen);
+
+ if (source === 'mainScanner') {
+ // If BottomSheet is not open yet (MainScanner)
+ showSheet('send', { screen });
+ } else {
+ // If BottomSheet is already open (SendScanner)
+ sendNavigation.navigate(screen);
+ }
updateBeignetSendTransaction({
label: message,
@@ -665,10 +673,13 @@ const handleData = async ({
slashTagsUrl,
});
- // If BottomSheet is not open yet (MainScanner)
- showBottomSheet('sendNavigation', { screen: 'Amount' });
- // If BottomSheet is already open (SendScanner)
- sendNavigation.navigate('Amount');
+ if (source === 'mainScanner') {
+ // If BottomSheet is not open yet (MainScanner)
+ showSheet('send', { screen: 'Amount' });
+ } else {
+ // If BottomSheet is already open (SendScanner)
+ sendNavigation.navigate('Amount');
+ }
return ok('');
}
@@ -683,10 +694,14 @@ const handleData = async ({
if (enableQuickpay && amount && amount < quickpayAmountSats) {
const screen = 'Quickpay';
const params = { invoice: lightningInvoice, amount };
- // If BottomSheet is not open yet (MainScanner)
- showBottomSheet('sendNavigation', { screen, ...params });
- // If BottomSheet is already open (SendScanner)
- sendNavigation.navigate(screen, params);
+
+ if (source === 'mainScanner') {
+ // If BottomSheet is not open yet (MainScanner)
+ showSheet('send', { screen, ...params });
+ } else {
+ // If BottomSheet is already open (SendScanner)
+ sendNavigation.navigate(screen, params);
+ }
return ok('');
}
@@ -695,10 +710,14 @@ const handleData = async ({
const invoiceAmount = amount ?? 0;
const screen = invoiceAmount ? 'ReviewAndSend' : 'Amount';
- // If BottomSheet is not open yet (MainScanner)
- showBottomSheet('sendNavigation', { screen });
- // If BottomSheet is already open (SendScanner)
- sendNavigation.navigate(screen);
+
+ if (source === 'mainScanner') {
+ // If BottomSheet is not open yet (MainScanner)
+ showSheet('send', { screen });
+ } else {
+ // If BottomSheet is already open (SendScanner)
+ sendNavigation.navigate(screen);
+ }
updateBeignetSendTransaction({
outputs: [{ address: '', value: invoiceAmount, index: 0 }],
@@ -710,7 +729,7 @@ const handleData = async ({
}
case EQRDataType.lnurlAddress:
case EQRDataType.lnurlPay: {
- const pParams = data.lnUrlParams! as LNURLPayParams;
+ const pParams = data.lnUrlParams;
//Convert msats to sats.
pParams.minSendable = Math.floor(pParams.minSendable / 1000);
@@ -719,6 +738,8 @@ const handleData = async ({
// Determine if we have enough sending capacity before proceeding.
const lightningBalance = getLightningBalance({ includeReserve: false });
+ dispatch(updateSendTransaction({ paymentMethod: 'lightning' }));
+
if (lightningBalance.localBalance < pParams.minSendable) {
showToast({
type: 'warning',
@@ -732,35 +753,52 @@ const handleData = async ({
if (pParams.minSendable === pParams.maxSendable) {
const amount = pParams.minSendable;
- showBottomSheet('sendNavigation', {
- screen: 'LNURLConfirm',
- pParams,
- amount,
- url: uri,
- });
+ if (source === 'mainScanner') {
+ // If BottomSheet is not open yet (MainScanner)
+ showSheet('send', {
+ screen: 'LNURLConfirm',
+ pParams,
+ amount,
+ url: uri,
+ });
+ } else {
+ // If BottomSheet is already open (SendScanner)
+ sendNavigation.navigate('LNURLConfirm', {
+ pParams,
+ amount,
+ url: uri,
+ });
+ }
sendNavigation.navigate('LNURLConfirm', {
pParams,
amount,
url: uri,
});
} else {
- showBottomSheet('sendNavigation', {
- screen: 'LNURLAmount',
- pParams: pParams,
- url: uri,
- });
- sendNavigation.navigate('LNURLAmount', { pParams, url: uri });
+ if (source === 'mainScanner') {
+ // If BottomSheet is not open yet (MainScanner)
+ showSheet('send', {
+ screen: 'LNURLAmount',
+ pParams: pParams,
+ url: uri,
+ });
+ } else {
+ // If BottomSheet is already open (SendScanner)
+ sendNavigation.navigate('LNURLAmount', { pParams, url: uri });
+ }
}
return ok('');
}
case EQRDataType.lnurlWithdraw: {
- const params = data.lnUrlParams as LNURLWithdrawParams;
+ const params = data.lnUrlParams;
//Convert msats to sats.
params.minWithdrawable = Math.floor(params.minWithdrawable / 1000);
params.maxWithdrawable = Math.floor(params.maxWithdrawable / 1000);
+ dispatch(updateSendTransaction({ paymentMethod: 'lightning' }));
+
if (params.minWithdrawable > params.maxWithdrawable) {
showToast({
type: 'warning',
@@ -786,11 +824,11 @@ const handleData = async ({
);
}
- showBottomSheet('lnurlWithdraw', { wParams: params });
+ showSheet('lnurlWithdraw', params);
return ok('');
}
case EQRDataType.lnurlChannel: {
- const params = data.lnUrlParams as LNURLChannelParams;
+ const params = data.lnUrlParams;
rootNavigation.navigate('TransferRoot', {
screen: 'LNURLChannel',
params: { cParams: params },
@@ -798,12 +836,12 @@ const handleData = async ({
return ok('');
}
case EQRDataType.lnurlAuth: {
- const params = data.lnUrlParams as LNURLAuthParams;
+ const params = data.lnUrlParams;
await handleLnurlAuth({ params, selectedWallet, selectedNetwork });
return ok('');
}
case EQRDataType.orangeTicket: {
- showBottomSheet('orangeTicket', { ticketId: data.ticketId });
+ showSheet('orangeTicket', { ticketId: data.ticketId });
return ok('');
}
case EQRDataType.nodeId: {
@@ -811,15 +849,15 @@ const handleData = async ({
screen: 'ExternalConnection',
params: { peer: data.uri },
});
- dispatch(closeSheet('sendNavigation'));
+ closeSheet('send');
return ok('');
}
case EQRDataType.treasureHunt: {
- showBottomSheet('treasureHunt', { chestId: data.chestId });
+ showSheet('treasureHunt', { chestId: data.chestId });
return ok('');
}
case EQRDataType.pubkyAuth: {
- showBottomSheet('pubkyAuth', { url: data.url });
+ showSheet('pubkyAuth', { url: data.url });
return ok('');
}
}
diff --git a/src/utils/slashtags/index.ts b/src/utils/slashtags/index.ts
index eff99f1bb..15c07964a 100644
--- a/src/utils/slashtags/index.ts
+++ b/src/utils/slashtags/index.ts
@@ -4,7 +4,6 @@ import { format, parse } from '@synonymdev/slashtags-url';
import debounce from 'lodash/debounce';
import { webRelayClient } from '../../components/SlashtagsProvider';
-import { rootNavigation } from '../../navigation/root/RootNavigationContainer';
import { dispatch, getSettingsStore } from '../../store/helpers';
import { updateSettings } from '../../store/slices/settings';
import {
@@ -38,17 +37,14 @@ import SlashpayConfig from './slashpay';
*/
export const handleSlashtagURL = (
url: string,
- onError?: (error: Error) => void,
onSuccess?: (url: string) => void,
+ onError?: (error: Error) => void,
): void => {
try {
const parsed = parse(url);
- if (parsed.protocol === 'slash:') {
- rootNavigation.navigate('ContactEdit', { url });
- } else {
- onError?.('Invalid URL' as unknown as Error);
- return;
+ if (parsed.protocol !== 'slash:') {
+ throw new Error('Invalid URL protocol - expected slash://');
}
onSuccess?.(url);