diff --git a/e2e/slashtags.e2e.js b/e2e/slashtags.e2e.js index 4ff9a7222..4712357fa 100644 --- a/e2e/slashtags.e2e.js +++ b/e2e/slashtags.e2e.js @@ -83,7 +83,9 @@ d('Profile and Contacts', () => { await element(by.id('ProfileAddLink')).tap(); await element(by.id('LinkLabelInput')).typeText('LINK-LABEL'); + await sleep(300); // wait for keyboard await element(by.id('LinkValueInput')).typeText('link-value'); + await sleep(300); // wait for keyboard await element(by.id('SaveLink')).tap(); await waitFor(element(by.id('SaveLink'))).not.toBeVisible(); await expect(element(by.text('LINK-LABEL'))).toExist(); @@ -137,6 +139,13 @@ d('Profile and Contacts', () => { .withTimeout(30000); await expect(element(by.text(satoshi.name))).toExist(); await expect(element(by.text(satoshi.bio))).toExist(); + + // Android: keyboard is not dismissed after adding contact in e2e + if (device.getPlatform() === 'android') { + await element(by.id('NameInput')).tapReturnKey(); + await sleep(300); // wait for keyboard to hide + } + await element(by.id('SaveContactButton')).tap(); await expect(element(by.text('WEBSITE'))).toExist(); await expect(element(by.text(satoshi.website))).toExist(); @@ -152,6 +161,14 @@ d('Profile and Contacts', () => { .withTimeout(30000); await expect(element(by.text(hal.name1))).toExist(); await element(by.id('NameInput')).replaceText(hal.name2); + await sleep(300); // wait for keyboard to hide + + // Android: keyboard is not dismissed after adding contact in e2e + if (device.getPlatform() === 'android') { + await element(by.id('NameInput')).tapReturnKey(); + await sleep(300); // wait for keyboard to hide + } + await element(by.id('SaveContactButton')).tap(); await expect(element(by.text(hal.name2.toUpperCase()))).toExist(); await element(by.id('NavigationClose')).tap(); diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx index 157dd7230..fdcda4857 100644 --- a/src/components/TabBar.tsx +++ b/src/components/TabBar.tsx @@ -19,7 +19,6 @@ import type { RootNavigationProp } from '../navigation/types'; import { useSheetRef } from '../sheets/SheetRefsProvider'; import { resetSendTransaction } from '../store/actions/wallet'; import { spendingOnboardingSelector } from '../store/reselect/aggregations'; -import { showSheet } from '../store/utils/ui'; import { ScanIcon } from '../styles/icons'; import ButtonBlur from './buttons/ButtonBlur'; @@ -34,13 +33,13 @@ const TabBar = (): ReactElement => { const onReceivePress = (): void => { const currentRoute = rootNavigation.getCurrentRoute(); + const screen = + // if we are on the spending screen and the user has not yet received funds + currentRoute === 'ActivitySpending' && isSpendingOnboarding + ? 'ReceiveAmount' + : 'ReceiveQR'; - // if we are on the spending screen and the user has not yet received funds - if (currentRoute === 'ActivitySpending' && isSpendingOnboarding) { - showSheet('receive', { screen: 'ReceiveAmount' }); - } else { - receiveSheetRef.current?.present(); - } + receiveSheetRef.current?.present({ screen }); }; const onSendPress = (): void => { diff --git a/src/components/Widgets.tsx b/src/components/Widgets.tsx index d87a97bc2..8824f259f 100644 --- a/src/components/Widgets.tsx +++ b/src/components/Widgets.tsx @@ -1,12 +1,6 @@ import { useFocusEffect } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native'; -import React, { - ReactElement, - memo, - useCallback, - useMemo, - useState, -} from 'react'; +import React, { ReactElement, memo, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { StyleSheet } from 'react-native'; import DraggableFlatList, { @@ -52,18 +46,6 @@ const Widgets = (): ReactElement => { useFocusEffect(useCallback(() => setEditing(false), [])); - const sortedWidgets = useMemo(() => { - const savedWidgets = Object.keys(widgets); - - const sorted = savedWidgets.sort((a, b) => { - const indexA = sortOrder.indexOf(a); - const indexB = sortOrder.indexOf(b); - return indexA - indexB; - }); - - return sorted; - }, [widgets, sortOrder]); - const onDragEnd = useCallback( ({ data }) => { const order = data.map((id): string => id); @@ -83,7 +65,7 @@ const Widgets = (): ReactElement => { ({ item: id, drag }: RenderItemParams): ReactElement => { const initiateDrag = (): void => { // only allow dragging if there are more than 1 widget - if (sortedWidgets.length > 1 && editing) { + if (sortOrder.length > 1 && editing) { drag(); } }; @@ -168,14 +150,14 @@ const Widgets = (): ReactElement => { /> ); }, - [editing, widgets, sortedWidgets.length], + [editing, widgets, sortOrder.length], ); return ( {t('widgets')} - {sortedWidgets.length > 0 && ( + {sortOrder.length > 0 && ( { id} renderItem={(params): ReactElement => ( {renderItem(params)} diff --git a/src/components/widgets/edit/FactsItems.tsx b/src/components/widgets/edit/FactsItems.tsx index 1936874a5..446228aed 100644 --- a/src/components/widgets/edit/FactsItems.tsx +++ b/src/components/widgets/edit/FactsItems.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { TFactsWidgetOptions } from '../../../store/types/widgets'; import { BodySSB, Title } from '../../../styles/text'; @@ -6,7 +7,7 @@ import { EWidgetItemType, TWidgetItem } from './types'; export const getFactsItems = (options: TFactsWidgetOptions): TWidgetItem[] => { const { t } = useTranslation('widgets'); - const fact = getRandomFact(); + const fact = useMemo(() => getRandomFact(), []); return [ { diff --git a/src/screens/Activity/ActivityDetail.tsx b/src/screens/Activity/ActivityDetail.tsx index 56264aa9d..b262c9308 100644 --- a/src/screens/Activity/ActivityDetail.tsx +++ b/src/screens/Activity/ActivityDetail.tsx @@ -66,6 +66,7 @@ import type { RootNavigationProp, RootStackScreenProps, } from '../../navigation/types'; +import { useSheetRef } from '../../sheets/SheetRefsProvider'; import { activityItemSelector, activityItemsSelector, @@ -85,7 +86,6 @@ import { deleteMetaTxTag, } from '../../store/slices/metadata'; import { ETransferStatus } from '../../store/types/wallet'; -import { showSheet } from '../../store/utils/ui'; import { getBoostedTransactionParents } from '../../utils/boost'; import { ellipsis, @@ -159,6 +159,8 @@ const OnchainActivityDetail = ({ const { wallet } = useOnchainWallet(); const { t } = useTranslation('wallet'); const { t: tTime } = useTranslation('intl', { i18n: i18nTime }); + const boostSheetRef = useSheetRef('boost'); + const activityTagsSheetRef = useSheetRef('activityTags'); const switchUnit = useSwitchUnit(); const dispatch = useAppDispatch(); const contacts = useAppSelector(contactsSelector); @@ -235,11 +237,11 @@ const OnchainActivityDetail = ({ }; const handleBoost = (): void => { - showSheet('boost', { activityItem: item }); + boostSheetRef.current?.present({ activityItem: item }); }; const handleAddTag = (): void => { - showSheet('activityTags', { id }); + activityTagsSheetRef.current?.present({ id }); }; const handleRemoveTag = (tag: string): void => { @@ -678,8 +680,10 @@ const LightningActivityDetail = ({ }): ReactElement => { const { t } = useTranslation('wallet'); const { t: tTime } = useTranslation('intl', { i18n: i18nTime }); + const activityTagsSheetRef = useSheetRef('activityTags'); const switchUnit = useSwitchUnit(); const colors = useColors(); + const { id, txType, @@ -700,7 +704,7 @@ const LightningActivityDetail = ({ }); const handleAddTag = (): void => { - showSheet('activityTags', { id }); + activityTagsSheetRef.current?.present({ id }); }; const handleRemoveTag = (tag: string): void => { diff --git a/src/screens/Contacts/Contact.tsx b/src/screens/Contacts/Contact.tsx index 9070e5cbc..5ecc0eb33 100644 --- a/src/screens/Contacts/Contact.tsx +++ b/src/screens/Contacts/Contact.tsx @@ -19,7 +19,6 @@ import { useBalance } from '../../hooks/wallet'; import { RootStackScreenProps } from '../../navigation/types'; import { contactsSelector } from '../../store/reselect/slashtags'; import { isLDKReadySelector } from '../../store/reselect/ui'; -import { selectedNetworkSelector } from '../../store/reselect/wallet'; import { deleteContact } from '../../store/slices/slashtags'; import { AnimatedView, View } from '../../styles/components'; import { @@ -45,7 +44,6 @@ const Contact = ({ const [loading, setLoading] = useState(false); const dispatch = useAppDispatch(); - const selectedNetwork = useAppSelector(selectedNetworkSelector); const contacts = useAppSelector(contactsSelector); const isLDKReady = useAppSelector(isLDKReadySelector); @@ -91,11 +89,7 @@ const Contact = ({ }); } - const res = await processUri({ - uri: url, - source: 'send', - selectedNetwork, - }); + const res = await processUri({ uri: url }); setLoading(false); if (res.isOk()) { navigation.popToTop(); diff --git a/src/screens/Settings/AddressViewer/index.tsx b/src/screens/Settings/AddressViewer/index.tsx index 96047e333..7c39f9cdf 100644 --- a/src/screens/Settings/AddressViewer/index.tsx +++ b/src/screens/Settings/AddressViewer/index.tsx @@ -43,7 +43,6 @@ 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 { showSheet } from '../../../store/utils/ui'; import { View as ThemedView, TouchableOpacity, @@ -620,6 +619,7 @@ const AddressViewer = (): ReactElement => { * The on-chain transaction will retrieve and include the app's receiving address by default. * Finally, this method will prompt the sendNavigation modal to appear for the user to finalize and confirm the transaction. */ + // biome-ignore lint/correctness/useExhaustiveDependencies: sheetRef doesn't change const onSpendFundsPress = useCallback( async (utxosLength, selectedUtxosLength): Promise => { if (utxosLength <= 0) { @@ -649,7 +649,7 @@ const AddressViewer = (): ReactElement => { }), ); sendMax(); - showSheet('send', { screen: 'ReviewAndSend' }); + sendSheetRef.current?.present({ screen: 'ReviewAndSend' }); }, [selectedUtxos, utxos, selectedNetwork, dispatch], ); diff --git a/src/screens/Settings/Security/index.tsx b/src/screens/Settings/Security/index.tsx index 074d3cee0..57db4e571 100644 --- a/src/screens/Settings/Security/index.tsx +++ b/src/screens/Settings/Security/index.tsx @@ -2,14 +2,13 @@ import React, { memo, ReactElement, useMemo, useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { StyleSheet } from 'react-native'; -import { View as ThemedView } from '../../../styles/components'; - import { IsSensorAvailableResult } from '../../../components/Biometrics'; import { EItemType, IListData } from '../../../components/List'; import { useAppDispatch, useAppSelector } from '../../../hooks/redux'; import type { SettingsScreenProps } from '../../../navigation/types'; +import { useSheetRef } from '../../../sheets/SheetRefsProvider'; import { updateSettings } from '../../../store/slices/settings'; -import { showSheet } from '../../../store/utils/ui'; +import { View as ThemedView } from '../../../styles/components'; import rnBiometrics from '../../../utils/biometrics'; import SettingsView from '../SettingsView'; @@ -18,6 +17,7 @@ const SecuritySettings = ({ }: SettingsScreenProps<'SecuritySettings'>): ReactElement => { const { t } = useTranslation('settings'); const dispatch = useAppDispatch(); + const pinSheetRef = useSheetRef('pinNavigation'); const [biometryData, setBiometricData] = useState(); const { enableAutoReadClipboard, @@ -53,6 +53,7 @@ const SecuritySettings = ({ ? t('security.footer', { biometryTypeName }) : undefined; + // biome-ignore lint/correctness/useExhaustiveDependencies: sheetRef doesn't change const settingsListData: IListData[] = useMemo( () => [ { @@ -119,7 +120,7 @@ const SecuritySettings = ({ if (pin) { navigation.navigate('DisablePin'); } else { - showSheet('pinNavigation', { showLaterButton: false }); + pinSheetRef.current?.present({ showLaterButton: false }); } }, }, diff --git a/src/screens/Transfer/Funding.tsx b/src/screens/Transfer/Funding.tsx index cb5ad55e1..a821dd02b 100644 --- a/src/screens/Transfer/Funding.tsx +++ b/src/screens/Transfer/Funding.tsx @@ -1,16 +1,16 @@ import React, { ReactElement } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { StyleSheet, View } from 'react-native'; -import { useAppSelector } from '../../hooks/redux'; import NavigationHeader from '../../components/NavigationHeader'; import SafeAreaInset from '../../components/SafeAreaInset'; import RectangleButton from '../../components/buttons/RectangleButton'; +import { useAppSelector } from '../../hooks/redux'; import { useBalance } from '../../hooks/wallet'; import type { TransferScreenProps } from '../../navigation/types'; +import { useSheetRef } from '../../sheets/SheetRefsProvider'; import { spendingIntroSeenSelector } from '../../store/reselect/settings'; import { isGeoBlockedSelector } from '../../store/reselect/user'; -import { showSheet } from '../../store/utils/ui'; import { View as ThemedView } from '../../styles/components'; import { QrIcon, ShareAndroidIcon, TransferIcon } from '../../styles/icons'; import { BodyM, Display } from '../../styles/text'; @@ -21,6 +21,7 @@ const Funding = ({ }: TransferScreenProps<'Funding'>): ReactElement => { const { t } = useTranslation('lightning'); const { onchainBalance } = useBalance(); + const receiveSheetRef = useSheetRef('receive'); const isGeoBlocked = useAppSelector(isGeoBlockedSelector); const spendingIntroSeen = useAppSelector(spendingIntroSeenSelector); @@ -34,7 +35,7 @@ const Funding = ({ const onFund = (): void => { navigation.popTo('Wallet', { screen: 'Home' }); - showSheet('receive', { screen: 'ReceiveAmount' }); + receiveSheetRef.current?.present({ screen: 'ReceiveAmount' }); }; const onAdvanced = (): void => { diff --git a/src/screens/Wallets/Receive/ReceiveDetails.tsx b/src/screens/Wallets/Receive/ReceiveDetails.tsx index d57d2788a..468ec566c 100644 --- a/src/screens/Wallets/Receive/ReceiveDetails.tsx +++ b/src/screens/Wallets/Receive/ReceiveDetails.tsx @@ -1,7 +1,7 @@ import React, { ReactElement, memo, useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Image, StyleSheet, View } from 'react-native'; -import { FadeIn, FadeOut } from 'react-native-reanimated'; +import { Image, Platform, StyleSheet, View } from 'react-native'; +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; import BottomSheetNavigationHeader from '../../../components/BottomSheetNavigationHeader'; import GradientView from '../../../components/GradientView'; @@ -27,7 +27,7 @@ import { updatePendingInvoice, } from '../../../store/slices/metadata'; import { removeInvoiceTag, updateInvoice } from '../../../store/slices/receive'; -import { AnimatedView, BottomSheetTextInput } from '../../../styles/components'; +import { BottomSheetTextInput } from '../../../styles/components'; import { TagIcon } from '../../../styles/icons'; import { Caption13Up } from '../../../styles/text'; import { estimateOrderFee } from '../../../utils/blocktank'; @@ -174,11 +174,12 @@ const ReceiveDetails = ({ {!keyboardShown && ( - + // FadeOut causing a bug on Android + exiting={Platform.OS === 'ios' ? FadeOut : undefined} + > {t('tags')} @@ -209,7 +210,7 @@ const ReceiveDetails = ({ )} - + )} diff --git a/src/sheets/AddContact.tsx b/src/sheets/AddContact.tsx index b32fa7be1..26872d19f 100644 --- a/src/sheets/AddContact.tsx +++ b/src/sheets/AddContact.tsx @@ -58,7 +58,7 @@ const AddContact = ({ }; const onSuccess = async (): Promise => { - navigation.navigate('ContactEdit', { url }); + navigation.navigate('ContactEdit', { url: contactUrl }); setUrl(''); }; diff --git a/src/store/slices/widgets.ts b/src/store/slices/widgets.ts index 87303793f..9c4193154 100644 --- a/src/store/slices/widgets.ts +++ b/src/store/slices/widgets.ts @@ -15,7 +15,7 @@ export const initialWidgetsState: TWidgetsState = { blocks: getDefaultOptions('blocks'), }, onboardedWidgets: false, - sortOrder: [], + sortOrder: ['price', 'news', 'blocks'], }; export const widgetsSlice = createSlice({ @@ -30,7 +30,9 @@ export const widgetsSlice = createSlice({ action: PayloadAction<{ id: string; options?: any }>, ) => { const { id, options } = action.payload; - state.sortOrder.push(id); + if (!state.sortOrder.includes(id)) { + state.sortOrder.push(id); + } state.widgets[id] = options ?? null; }, deleteWidget: (state, action: PayloadAction) => {