Compare commits

..

1 Commits

Author SHA1 Message Date
Ivan Vershigora
02ccdd66c3
REF: swap crypto-js for @noble/ciphers + hashes 2026-06-18 19:53:25 +01:00
15 changed files with 184 additions and 429 deletions

View File

@ -7,7 +7,7 @@ gem "fastlane", "~> 2.234.0"
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem 'xcodeproj', '< 1.26.0'
gem 'concurrent-ruby', '< 1.3.8'
gem 'concurrent-ruby', '< 1.3.4'
# Ruby 3.4.0 removed these from the standard library
gem 'bigdecimal'

View File

@ -87,7 +87,7 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.3.7)
concurrent-ruby (1.3.3)
connection_pool (3.0.2)
csv (3.3.5)
declarative (0.0.20)
@ -337,7 +337,7 @@ DEPENDENCIES
benchmark
bigdecimal
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
concurrent-ruby (< 1.3.8)
concurrent-ruby (< 1.3.4)
fastlane (~> 2.234.0)
fastlane-plugin-browserstack
fastlane-plugin-bugsnag
@ -377,7 +377,7 @@ CHECKSUMS
colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c
colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a
commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9
concurrent-ruby (1.3.7) sha256=4412caec3a5ea2e5fdc52076724c071a81f2c0593d83b2ac8cbb8ca63b3151b0
concurrent-ruby (1.3.3) sha256=4f9cd28965c4dcf83ffd3ea7304f9323277be8525819cb18a3b61edcb56a7c6a
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9

View File

@ -0,0 +1,34 @@
import { md5 } from '@noble/hashes/legacy';
import { concatBytes } from '@noble/hashes/utils';
/**
* OpenSSL EVP_BytesToKey using MD5 with 1 iteration.
*
* Reproduces the default key+IV derivation used by CryptoJS@4.x's
* `AES.encrypt(string, password)` so the on-disk wire format stays
* bit-identical after we swap the underlying library.
*
* D1 = MD5( password || salt )
* Di = MD5( D(i-1) || password || salt ) for i 2
* key||iv = D1 || D2 || ... (take first `byteLength` bytes)
*
* MD5 is intentional: it matches the legacy OpenSSL format. The
* cryptographic weakness of MD5 is not relevant here the function is
* only used as a deterministic byte-stretcher; the password's entropy is
* what protects the wallet, not MD5.
*/
export function evpBytesToKeyMd5(password: Uint8Array, salt: Uint8Array, byteLength: number): Uint8Array {
if (!Number.isInteger(byteLength) || byteLength < 0) {
throw new Error('evpBytesToKeyMd5: byteLength must be a non-negative integer');
}
const out = new Uint8Array(byteLength);
let written = 0;
let prev: Uint8Array = new Uint8Array(0);
while (written < byteLength) {
prev = md5(concatBytes(prev, password, salt));
const take = Math.min(prev.length, byteLength - written);
out.set(prev.subarray(0, take), written);
written += take;
}
return out;
}

View File

@ -1,40 +1,8 @@
import { cbc } from '@noble/ciphers/aes';
import { md5 } from '@noble/hashes/legacy';
import { randomBytes } from '@noble/hashes/utils';
import { areUint8ArraysEqual, base64ToUint8Array, concatUint8Arrays, stringToUint8Array, uint8ArrayToBase64 } from './uint8array-extras';
/**
* OpenSSL EVP_BytesToKey using MD5 with 1 iteration.
*
* Reproduces the default key+IV derivation used by CryptoJS@4.x's
* `AES.encrypt(string, password)` so the on-disk wire format stays
* bit-identical after we swap the underlying library.
*
* D1 = MD5( password || salt )
* Di = MD5( D(i-1) || password || salt ) for i 2
* key||iv = D1 || D2 || ... (take first `byteLength` bytes)
*
* MD5 is intentional: it matches the legacy OpenSSL format. The
* cryptographic weakness of MD5 is not relevant here the function is
* only used as a deterministic byte-stretcher; the password's entropy is
* what protects the wallet, not MD5.
*/
export function evpBytesToKeyMd5(password: Uint8Array, salt: Uint8Array, byteLength: number): Uint8Array {
if (!Number.isInteger(byteLength) || byteLength < 0) {
throw new Error('evpBytesToKeyMd5: byteLength must be a non-negative integer');
}
const out = new Uint8Array(byteLength);
let written = 0;
let prev: Uint8Array = new Uint8Array(0);
while (written < byteLength) {
prev = md5(concatUint8Arrays([prev, password, salt]));
const take = Math.min(prev.length, byteLength - written);
out.set(prev.subarray(0, take), written);
written += take;
}
return out;
}
import { evpBytesToKeyMd5 } from './crypto/evp_bytes_to_key';
// "Salted__" — OpenSSL envelope magic. Hardcoded as bytes so the wire
// format cannot drift through any encoder.

View File

@ -52,14 +52,7 @@ const useFloatButtonAnimation = (initialHeight: number) => {
};
};
const getScaledButtonHeight = (fontScale: number): number => Math.round(LAYOUT.BUTTON_HEIGHT * fontScale);
/** Scroll padding so list content clears float buttons (excludes safe-area inset). Default 70 at fontScale 1. */
const FLOAT_BUTTON_LIST_CLEARANCE = 18;
export const getFloatingButtonReservedHeight = (fontScale = 1): number => getScaledButtonHeight(fontScale) + FLOAT_BUTTON_LIST_CLEARANCE;
const useFloatButtonLayout = (width: number, sizeClass: SizeClass, fontScale: number) => {
const useFloatButtonLayout = (width: number, sizeClass: SizeClass) => {
const lastVerticalDecision = useRef(false);
const shouldUseVerticalLayout = useCallback(
@ -159,19 +152,15 @@ const useFloatButtonLayout = (width: number, sizeClass: SizeClass, fontScale: nu
[width, sizeClass, shouldUseVerticalLayout],
);
const calculateContainerHeight = useCallback(
(childrenCount: number, isVerticalLayout: boolean) => {
const buttonHeight = getScaledButtonHeight(fontScale);
if (!isVerticalLayout) return { height: '8%', minHeight: buttonHeight };
const calculateContainerHeight = useCallback((childrenCount: number, isVerticalLayout: boolean) => {
if (!isVerticalLayout) return { height: '8%', minHeight: LAYOUT.BUTTON_HEIGHT };
const totalButtonsHeight = childrenCount * buttonHeight;
const totalMarginsHeight = (childrenCount - 1) * LAYOUT.BUTTON_MARGIN;
const calculatedHeight = totalButtonsHeight + totalMarginsHeight;
const totalButtonsHeight = childrenCount * LAYOUT.BUTTON_HEIGHT;
const totalMarginsHeight = (childrenCount - 1) * LAYOUT.BUTTON_MARGIN;
const calculatedHeight = totalButtonsHeight + totalMarginsHeight;
return { height: calculatedHeight };
},
[fontScale],
);
return { height: calculatedHeight };
}, []);
const calculateButtonFontSize = useMemo(() => {
const divisor = sizeClass === SizeClass.Large ? 22 : sizeClass === SizeClass.Regular ? 24 : 28;
@ -278,7 +267,6 @@ interface FButtonProps {
isVertical?: boolean;
borderRadius?: number;
fontSize?: number;
buttonHeight?: number;
disabled?: boolean;
testID?: string;
onPress: () => void;
@ -289,14 +277,13 @@ interface ButtonContentProps {
icon: ReactNode;
text: string;
textStyle: StyleProp<TextStyle>;
buttonHeight: number;
}
const getScaledIconSize = (fontSize: number): number => {
return Math.max(Math.round(fontSize * 1.2), 16);
};
const ButtonContent = ({ icon, text, textStyle, buttonHeight }: ButtonContentProps) => {
const ButtonContent = ({ icon, text, textStyle }: ButtonContentProps) => {
const computedStyle = StyleSheet.flatten(textStyle);
const fontSize = computedStyle.fontSize || LAYOUT.MAX_BUTTON_FONT_SIZE;
const iconSize = getScaledIconSize(Number(fontSize));
@ -320,14 +307,9 @@ const ButtonContent = ({ icon, text, textStyle, buttonHeight }: ButtonContentPro
}
return (
<View style={[buttonContentStaticStyles.contentContainer, { minHeight: buttonHeight }]}>
<View style={buttonContentStaticStyles.contentContainer}>
<View style={buttonStyles.iconContainer}>{scaledIcon}</View>
<Text
numberOfLines={1}
adjustsFontSizeToFit
minimumFontScale={0.8}
style={[textStyle, buttonStyles.centeredText, { lineHeight: fontSize * 1.2 }]}
>
<Text numberOfLines={1} adjustsFontSizeToFit style={[textStyle, buttonStyles.centeredText, { lineHeight: fontSize * 1.2 }]}>
{text}
</Text>
</View>
@ -343,7 +325,6 @@ export const FButton = ({
isVertical,
borderRadius = LAYOUT.PILL_BORDER_RADIUS,
fontSize = LAYOUT.MAX_BUTTON_FONT_SIZE,
buttonHeight = LAYOUT.BUTTON_HEIGHT,
testID,
...props
}: FButtonProps) => {
@ -366,8 +347,6 @@ export const FButton = ({
return {
root: {
...baseStyles,
height: buttonHeight,
minHeight: buttonHeight,
backgroundColor: colors.buttonBackgroundColor,
},
text: {
@ -381,7 +360,7 @@ export const FButton = ({
marginBottom: buttonContentStaticStyles.marginBottom,
textBase: buttonContentStaticStyles.textBase,
};
}, [colors, fontSize, buttonHeight]);
}, [colors, fontSize]);
const style: Record<string, any> = {};
const additionalStyles = !last ? (isVertical ? customButtonStyles.marginBottom : customButtonStyles.marginRight) : {};
@ -418,7 +397,7 @@ export const FButton = ({
style={[buttonStyles.root, customButtonStyles.root, style, { borderRadius }]}
{...props}
>
<ButtonContent icon={icon} text={text} textStyle={textStyle} buttonHeight={buttonHeight} />
<ButtonContent icon={icon} text={text} textStyle={textStyle} />
</TouchableOpacity>
</Animated.View>
);
@ -426,9 +405,8 @@ export const FButton = ({
export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
const insets = useSafeAreaInsets();
const { height, width, fontScale } = useWindowDimensions();
const { height, width } = useWindowDimensions();
const { sizeClass } = useSizeClass();
const scaledButtonHeight = getScaledButtonHeight(fontScale);
const childrenCount = React.Children.toArray(props.children).filter(Boolean).length;
@ -441,7 +419,6 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
const { calculateButtonWidth, calculateVisualParameters, calculateContainerHeight, buttonFontSize } = useFloatButtonLayout(
width,
sizeClass,
fontScale,
);
// Compute initial geometry up-front so the slide-in animation starts at the final (computed) size,
@ -531,7 +508,7 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
useEffect(() => {
debouncedCalculateLayout();
}, [debouncedCalculateLayout, width, height, childrenCount, sizeClass, fontScale]);
}, [debouncedCalculateLayout, width, height, childrenCount, sizeClass]);
const onLayout = (event: { nativeEvent: { layout: { width: number } } }) => {
const { width: currentLayoutWidth } = event.nativeEvent.layout;
@ -568,7 +545,6 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
isVertical,
borderRadius: buttonBorderRadius,
fontSize: buttonFontSize,
buttonHeight: scaledButtonHeight,
});
};
@ -585,10 +561,10 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
props.inline ? containerStyles.rootInline : containerStyles.rootAbsolute,
bottomInsets,
effectiveNewWidth ? (isVertical ? containerStyles.rootPostVertical : containerStyles.rootPost) : containerStyles.rootPre,
isVertical ? containerHeight : { minHeight: scaledButtonHeight },
isVertical ? containerHeight : null,
{ transform: [{ translateY: slideAnimation }] },
],
[props.inline, bottomInsets, effectiveNewWidth, isVertical, containerHeight, slideAnimation, scaledButtonHeight],
[props.inline, bottomInsets, effectiveNewWidth, isVertical, containerHeight, slideAnimation],
);
return (

View File

@ -1,13 +1,10 @@
import React, { useMemo } from 'react';
import { Pressable, StyleProp, StyleSheet, Switch, SwitchProps, Text, TextStyle, useWindowDimensions, View, ViewStyle } from 'react-native';
import { Pressable, StyleProp, StyleSheet, Switch, SwitchProps, Text, TextStyle, View, ViewStyle } from 'react-native';
import { useLocale } from '@react-navigation/native';
import Icon from './Icon';
import { useTheme } from './themes';
/** Base row height for transaction list `getItemLayout` (padding + title + subtitle at fontScale 1). */
export const TX_ROW_BASE_HEIGHT = 64;
interface ListItemProps {
leftAvatar?: React.JSX.Element;
containerStyle?: StyleProp<ViewStyle>;
@ -58,20 +55,12 @@ const ListItem: React.FC<ListItemProps> = React.memo(
}: ListItemProps) => {
const { colors } = useTheme();
const { direction } = useLocale();
const { fontScale } = useWindowDimensions();
const isRtl = direction === 'rtl';
const contentRowStyle = useMemo(
() => ({
paddingVertical: Math.round(12 * fontScale),
}),
[fontScale],
);
const stylesHook = StyleSheet.create({
title: {
color: disabled ? colors.buttonDisabledTextColor : colors.foregroundColor,
fontSize: 16,
fontWeight: '500',
lineHeight: Math.round(22 * fontScale),
writingDirection: direction,
},
rightMemoText: {
@ -83,7 +72,7 @@ const ListItem: React.FC<ListItemProps> = React.memo(
color: colors.alternativeTextColor,
fontWeight: '400',
paddingVertical: switchProps ? 8 : 0,
lineHeight: Math.round(20 * fontScale),
lineHeight: 20,
fontSize: 14,
marginTop: 2,
},
@ -104,7 +93,7 @@ const ListItem: React.FC<ListItemProps> = React.memo(
const enableFeedback = !noFeedback && !!onPress && !disabled;
const renderContent = () => (
<View style={[styles.contentRow, contentRowStyle]}>
<View style={styles.contentRow}>
{leftAvatar && (
<View style={styles.leftAvatarContainer}>
{leftAvatar}
@ -125,14 +114,7 @@ const ListItem: React.FC<ListItemProps> = React.memo(
{rightTitle || rightSubtitle ? (
<View style={styles.rightColumn}>
{rightTitle ? (
<Text
style={rightTitleStyle}
numberOfLines={1}
adjustsFontSizeToFit
minimumFontScale={0.75}
accessibilityRole="text"
selectable={rightTitleSelectable}
>
<Text style={rightTitleStyle} numberOfLines={1} accessibilityRole="text" selectable={rightTitleSelectable}>
{rightTitle}
</Text>
) : null}
@ -210,20 +192,16 @@ const styles = StyleSheet.create({
},
content: {
flex: 1,
flexShrink: 1,
minWidth: 0,
justifyContent: 'center',
},
leftAvatarContainer: {
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'center',
},
rightColumn: {
marginStart: 8,
flexShrink: 0,
minWidth: 0,
alignItems: 'flex-end',
alignSelf: 'center',
},
rightMemoWrapper: {
flexShrink: 1,

View File

@ -67,25 +67,26 @@ const ReplaceFeeSuggestions: React.FC<ReplaceFeeSuggestionsProps> = ({ onFeeSele
}, []);
const handleFeeSelection = (feeType: NetworkTransactionFeeType) => {
if (feeType === NetworkTransactionFeeType.CUSTOM) {
setSelectedFeeType(feeType);
return;
if (feeType !== NetworkTransactionFeeType.CUSTOM) {
Keyboard.dismiss();
}
Keyboard.dismiss();
if (networkFees) {
let selectedFee: number;
switch (feeType) {
case NetworkTransactionFeeType.FAST:
onFeeSelected(networkFees.fastestFee);
selectedFee = networkFees.fastestFee;
break;
case NetworkTransactionFeeType.MEDIUM:
onFeeSelected(networkFees.mediumFee);
selectedFee = networkFees.mediumFee;
break;
case NetworkTransactionFeeType.SLOW:
onFeeSelected(networkFees.slowFee);
selectedFee = networkFees.slowFee;
break;
case NetworkTransactionFeeType.CUSTOM:
selectedFee = Number(customFeeValue);
break;
}
onFeeSelected(selectedFee);
setSelectedFeeType(feeType);
}
};
@ -93,8 +94,7 @@ const ReplaceFeeSuggestions: React.FC<ReplaceFeeSuggestionsProps> = ({ onFeeSele
const handleCustomFeeChange = (customFee: string) => {
const sanitizedFee = customFee.replace(/[^0-9]/g, '');
setCustomFeeValue(sanitizedFee);
onFeeSelected(Number(sanitizedFee));
setSelectedFeeType(NetworkTransactionFeeType.CUSTOM);
handleFeeSelection(NetworkTransactionFeeType.CUSTOM);
};
return (
@ -156,10 +156,7 @@ const ReplaceFeeSuggestions: React.FC<ReplaceFeeSuggestionsProps> = ({ onFeeSele
ref={customTextInput}
maxLength={9}
style={[styles.customFeeInput, stylesHook.customFeeInput]}
onFocus={() => {
setSelectedFeeType(NetworkTransactionFeeType.CUSTOM);
onFeeSelected(Number(customFeeValue));
}}
onFocus={() => handleCustomFeeChange(customFeeValue)}
placeholder={loc.send.fee_satvbyte}
placeholderTextColor="#81868e"
inputAccessoryViewID={DismissKeyboardInputAccessoryViewID}

View File

@ -1,5 +1,5 @@
import React, { useMemo, useCallback } from 'react';
import { TouchableOpacity, Text, StyleSheet, View, useWindowDimensions } from 'react-native';
import { TouchableOpacity, Text, StyleSheet, View } from 'react-native';
import { useStorage } from '../hooks/context/useStorage';
import loc, { formatBalanceWithoutSuffix } from '../loc';
import { BitcoinUnit } from '../models/bitcoinUnits';
@ -22,7 +22,6 @@ const TotalWalletsBalance: React.FC = React.memo(() => {
setTotalBalancePreferredUnitStorage,
} = useSettings();
const { colors } = useTheme();
const { fontScale } = useWindowDimensions();
const totalBalanceFormatted = useMemo(() => {
const totalBalance = wallets.reduce((prev, curr) => {
@ -32,22 +31,6 @@ const TotalWalletsBalance: React.FC = React.memo(() => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wallets, totalBalancePreferredUnit, preferredFiatCurrency]);
const scaledStyles = useMemo(
() => ({
container: {
paddingVertical: Math.round(8 * fontScale),
},
label: {
lineHeight: Math.round(18 * fontScale),
marginBottom: Math.round(2 * fontScale),
},
balance: {
lineHeight: Math.round(38 * Math.max(1, fontScale)),
},
}),
[fontScale],
);
const toolTipActions = useMemo(
() => [
{
@ -109,20 +92,13 @@ const TotalWalletsBalance: React.FC = React.memo(() => {
return (
<ToolTipMenu actions={toolTipActions} onPressMenuItem={onPressMenuItem} shouldOpenOnLongPress style={styles.menuContainer}>
<View style={[styles.container, scaledStyles.container]}>
<Text style={[styles.label, scaledStyles.label]} numberOfLines={1} adjustsFontSizeToFit minimumFontScale={0.8}>
{loc.wallets.total_balance}
</Text>
<TouchableOpacity onPress={handleBalanceOnPress} style={styles.balanceTouchable}>
<Text
style={[styles.balance, scaledStyles.balance, { color: colors.foregroundColor }]}
numberOfLines={1}
adjustsFontSizeToFit
minimumFontScale={0.55}
>
{totalBalanceFormatted}
<View style={styles.container}>
<Text style={styles.label}>{loc.wallets.total_balance}</Text>
<TouchableOpacity onPress={handleBalanceOnPress}>
<Text style={[styles.balance, { color: colors.foregroundColor }]}>
{totalBalanceFormatted}{' '}
{totalBalancePreferredUnit !== BitcoinUnit.LOCAL_CURRENCY && (
<Text style={[styles.currency, { color: colors.foregroundColor }]}>{` ${totalBalancePreferredUnit}`}</Text>
<Text style={[styles.currency, { color: colors.foregroundColor }]}>{totalBalancePreferredUnit}</Text>
)}
</Text>
</TouchableOpacity>
@ -140,11 +116,6 @@ const styles = StyleSheet.create({
alignItems: 'flex-start',
paddingHorizontal: 16,
paddingVertical: 8,
width: '100%',
},
balanceTouchable: {
alignSelf: 'stretch',
width: '100%',
},
label: {
fontSize: 14,
@ -154,7 +125,6 @@ const styles = StyleSheet.create({
balance: {
fontSize: 32,
fontWeight: 'bold',
lineHeight: 38,
},
currency: {
fontSize: 18,

View File

@ -1,7 +1,7 @@
import React, { memo, useCallback, useMemo, useRef } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import Clipboard from '@react-native-clipboard/clipboard';
import { Animated, Easing, Linking, Pressable, Text, TextStyle, ViewStyle, StyleSheet, View, useWindowDimensions } from 'react-native';
import { Animated, Easing, Linking, Pressable, Text, TextStyle, ViewStyle, StyleSheet, View } from 'react-native';
import Lnurl from '../class/lnurl';
import { LightningArkWallet } from '../class/wallets/lightning-ark-wallet';
import { LightningTransaction, Transaction } from '../class/wallets/types';
@ -29,6 +29,9 @@ import { uint8ArrayToHex } from '../blue_modules/uint8array-extras';
import ListItem from './ListItem';
const styles = StyleSheet.create({
dateLine: {
fontSize: 13,
},
fullWidthButton: {
width: '100%',
alignSelf: 'stretch',
@ -130,7 +133,6 @@ const TransactionListItemComponent: React.FC<TransactionListItemProps> = ({
const { txMetadata, counterpartyMetadata, wallets } = useStorage();
const { language, selectedBlockExplorer } = useSettings();
const insets = useSafeAreaInsets();
const { fontScale } = useWindowDimensions();
const containerStyle = useMemo(
() => ({
backgroundColor: colors.background,
@ -246,7 +248,6 @@ const TransactionListItemComponent: React.FC<TransactionListItemProps> = ({
color,
fontSize: 14,
fontWeight: '600' as TextStyle['fontWeight'],
lineHeight: Math.round(20 * fontScale),
textAlign: 'right',
paddingRight: insets.right,
paddingLeft: insets.left,
@ -261,7 +262,6 @@ const TransactionListItemComponent: React.FC<TransactionListItemProps> = ({
item.ispaid,
insets.right,
insets.left,
fontScale,
]);
const determineTransactionTypeAndAvatar = () => {
@ -549,7 +549,7 @@ const TransactionListItemComponent: React.FC<TransactionListItemProps> = ({
<ListItem
leftAvatar={avatar}
title={listTitle}
subtitle={dateLine}
subtitle={<Text style={styles.dateLine}>{dateLine}</Text>}
chevron={false}
rightTitle={rowTitle}
rightTitleStyle={rowTitleStyle}

View File

@ -254,7 +254,6 @@ const styles = StyleSheet.create({
position: 'relative',
},
contentContainer: {
flex: 1,
paddingTop: WALLET_LABEL_TOP_GAP,
paddingHorizontal: 16,
paddingBottom: HERO_BOTTOM_PADDING,

View File

@ -30,7 +30,6 @@ import WalletGradient from '../class/wallet-gradient';
import { useSizeClass, SizeClass } from '../blue_modules/sizeClass';
import loc, { formatBalance, transactionTimeToReadable } from '../loc';
import { BlurredBalanceView } from './BlurredBalanceView';
import { withAlpha } from './color';
import { useTheme } from './themes';
import { Transaction, TWallet } from '../class/wallets/types';
import { BlueSpacing10 } from './BlueSpacing';
@ -38,30 +37,6 @@ import { useLocale } from '@react-navigation/native';
export const WALLET_CAROUSEL_HEADER_WIDTH = 16;
/** Base card body height at default Dynamic Type — grows with larger Dynamic Type, never shrinks below default. */
export const WALLET_CARD_BASE_MIN_HEIGHT = 164;
/** Top inset above wallet cards in the horizontal home carousel. */
export const WALLET_CAROUSEL_PADDING_TOP = 12;
/** Bottom inset so iOS card shadows (offset 4 + radius 8) are not clipped by the list row. */
export const WALLET_CAROUSEL_PADDING_BOTTOM = 20;
/** Scale layout metrics up for accessibility sizes; keep the design default when fontScale ≤ 1. */
const scaleLayoutUp = (base: number, fontScale: number): number => Math.round(base * Math.max(1, fontScale));
export const getWalletCardMinHeight = (fontScale = 1): number => scaleLayoutUp(WALLET_CARD_BASE_MIN_HEIGHT, fontScale);
export const getWalletCarouselHeight = (fontScale = 1): number =>
scaleLayoutUp(WALLET_CAROUSEL_PADDING_TOP, fontScale) +
getWalletCardMinHeight(fontScale) +
scaleLayoutUp(WALLET_CAROUSEL_PADDING_BOTTOM, fontScale);
/** Default carousel row height at `fontScale` 1 — prefer `getWalletCarouselHeight(fontScale)` when layout depends on Dynamic Type. */
export const WALLET_CAROUSEL_HEIGHT = getWalletCarouselHeight(1);
/** Vertical gap between the wallet title/balance block and the latest-tx footer on carousel cards. */
const WALLET_CARD_SECTION_GAP = 12;
const WALLET_CARD_TEXT_OPACITY = 0.85;
export const getWalletCarouselItemWidth = (screenWidth: number) => Math.round(screenWidth * 0.82 > 375 ? 375 : screenWidth * 0.82);
interface NewWalletPanelProps {
@ -185,28 +160,23 @@ const iStyles = StyleSheet.create({
borderRadius: 12,
minHeight: 164,
overflow: 'hidden',
justifyContent: 'flex-end',
},
gradCompact: {
borderRadius: 10,
minHeight: 132,
overflow: 'hidden',
justifyContent: 'flex-end',
},
gradContent: {
padding: 15,
width: '100%',
},
gradContentCompact: {
padding: 12,
},
balanceContainer: {
minHeight: 40,
justifyContent: 'center',
height: 40,
},
balanceContainerCompact: {
minHeight: 32,
justifyContent: 'center',
height: 32,
},
image: {
width: 99,
@ -219,6 +189,9 @@ const iStyles = StyleSheet.create({
width: 78,
height: 74,
},
br: {
backgroundColor: 'transparent',
},
label: {
backgroundColor: 'transparent',
fontSize: 19,
@ -233,6 +206,7 @@ const iStyles = StyleSheet.create({
},
balanceCompact: {
fontSize: 28,
lineHeight: 34,
},
latestTx: {
backgroundColor: 'transparent',
@ -308,32 +282,11 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
const balanceOpacity = useSharedValue(1);
const balanceTranslateY = useSharedValue(0);
const { colors } = useTheme();
const { width, fontScale } = useWindowDimensions();
const { width } = useWindowDimensions();
const itemWidth = getWalletCarouselItemWidth(width);
const { sizeClass } = useSizeClass();
const isCompact = sizeVariant === 'compact';
const { direction } = useLocale();
const scaledCardStyles = useMemo(
() => ({
grad: { minHeight: getWalletCardMinHeight(fontScale) },
gradContent: { padding: scaleLayoutUp(15, fontScale) },
balanceContainer: { minHeight: scaleLayoutUp(40, fontScale) },
textSpacer: { height: scaleLayoutUp(WALLET_CARD_SECTION_GAP, fontScale) },
label: { lineHeight: scaleLayoutUp(24, fontScale) },
balance: { lineHeight: scaleLayoutUp(38, fontScale) },
balanceCompact: { lineHeight: scaleLayoutUp(30, fontScale) },
latestTx: { lineHeight: scaleLayoutUp(18, fontScale) },
latestTxTime: { lineHeight: scaleLayoutUp(22, fontScale) },
}),
[fontScale],
);
const cardTextStyle = useMemo(
() => ({
color: withAlpha(colors.inverseForegroundColor, WALLET_CARD_TEXT_OPACITY),
writingDirection: direction,
}),
[colors.inverseForegroundColor, direction],
);
const previousBalance = useRef<string | undefined>(undefined);
const balance = !hideBalance && formatBalance(Number(item.getBalance()), item.getPreferredBalanceUnit(), true);
const safeBalance = balance || undefined;
@ -478,23 +431,23 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
{ backgroundColor: colors.background, shadowColor: colors.shadowColor },
]}
>
<LinearGradient
colors={WalletGradient.gradientsFor(item.type)}
style={[iStyles.grad, isCompact && iStyles.gradCompact, scaledCardStyles.grad]}
>
<LinearGradient colors={WalletGradient.gradientsFor(item.type)} style={[iStyles.grad, isCompact && iStyles.gradCompact]}>
<ImageBackground source={image} style={[iStyles.image, isCompact && iStyles.imageCompact]} />
<View style={[iStyles.gradContent, isCompact && iStyles.gradContentCompact, !isCompact && scaledCardStyles.gradContent]}>
<View style={[iStyles.gradContent, isCompact && iStyles.gradContentCompact]}>
<Text style={iStyles.br} />
{!isPlaceHolder && (
<>
<Text
numberOfLines={1}
style={[iStyles.label, isCompact && iStyles.labelCompact, scaledCardStyles.label, cardTextStyle]}
style={[
iStyles.label,
isCompact && iStyles.labelCompact,
{ color: colors.inverseForegroundColor, writingDirection: direction },
]}
>
{renderHighlightedText ? renderHighlightedText(walletLabel, searchQuery || '') : walletLabel}
</Text>
<View
style={[iStyles.balanceContainer, isCompact && iStyles.balanceContainerCompact, scaledCardStyles.balanceContainer]}
>
<View style={[iStyles.balanceContainer, isCompact && iStyles.balanceContainerCompact]}>
{hideBalance ? (
<>
<BlueSpacing10 />
@ -504,13 +457,11 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
<Animated.Text
numberOfLines={1}
adjustsFontSizeToFit
minimumFontScale={0.55}
key={`${balance}`} // force component recreation on balance change. To fix right-to-left languages, like Farsi
style={[
iStyles.balance,
isCompact && iStyles.balanceCompact,
isCompact ? scaledCardStyles.balanceCompact : scaledCardStyles.balance,
cardTextStyle,
{ color: colors.inverseForegroundColor, writingDirection: direction },
animatedBalanceStyle,
]}
>
@ -518,20 +469,24 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
</Animated.Text>
)}
</View>
<View style={scaledCardStyles.textSpacer} />
<Text style={iStyles.br} />
<Text
numberOfLines={1}
adjustsFontSizeToFit
minimumFontScale={0.8}
style={[iStyles.latestTx, isCompact && iStyles.latestTxCompact, scaledCardStyles.latestTx, cardTextStyle]}
style={[
iStyles.latestTx,
isCompact && iStyles.latestTxCompact,
{ color: colors.inverseForegroundColor, writingDirection: direction },
]}
>
{loc.wallets.list_latest_transaction}
</Text>
<Text
numberOfLines={1}
adjustsFontSizeToFit
minimumFontScale={0.8}
style={[iStyles.latestTxTime, isCompact && iStyles.latestTxTimeCompact, scaledCardStyles.latestTxTime, cardTextStyle]}
style={[
iStyles.latestTxTime,
isCompact && iStyles.latestTxTimeCompact,
{ color: colors.inverseForegroundColor, writingDirection: direction },
]}
>
{latestTransactionText}
</Text>
@ -586,7 +541,7 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
animateChanges = false,
} = props;
const { width, fontScale } = useWindowDimensions();
const { width } = useWindowDimensions();
const itemWidth = React.useMemo(() => getWalletCarouselItemWidth(width), [width]);
const snapInterval = React.useMemo(() => itemWidth, [itemWidth]);
const snapOffsets = React.useMemo(() => {
@ -695,7 +650,7 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
console.warn('[WalletsCarousel] Error scrolling to wallet:', error);
// Fallback: try scrolling to offset
// Use different measurement based on orientation
const itemSize = horizontal ? itemWidth : WALLET_CAROUSEL_HEIGHT;
const itemSize = horizontal ? itemWidth : 195; // 195 is the approximate height of wallet card
flatListRef.current.scrollToOffset({
offset: itemSize * walletIndex,
animated,
@ -817,7 +772,7 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
const keyExtractor = useCallback((item: TWallet, index: number) => (item?.getID ? item.getID() : index.toString()), []);
const sliderHeight = getWalletCarouselHeight(fontScale);
const sliderHeight = 195;
useEffect(() => {
return () => {
@ -900,8 +855,7 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
const cStyles = StyleSheet.create({
content: {
paddingTop: scaleLayoutUp(WALLET_CAROUSEL_PADDING_TOP, fontScale),
paddingBottom: scaleLayoutUp(WALLET_CAROUSEL_PADDING_BOTTOM, fontScale),
paddingTop: 16,
},
contentLargeScreen: {
paddingHorizontal: sizeClass === SizeClass.Large ? 16 : 12,
@ -932,7 +886,7 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
automaticallyAdjustContentInsets
automaticallyAdjustKeyboardInsets
automaticallyAdjustsScrollIndicatorInsets
style={{ minHeight: sliderHeight }}
style={{ minHeight: sliderHeight + 12 }}
onScrollToIndexFailed={onScrollToIndexFailed}
ListFooterComponent={onNewWalletPress ? <NewWalletPanel onPress={onNewWalletPress} /> : null}
{...props}

View File

@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState }
import { ActivityIndicator, BackHandler, Linking, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native';
import { sha256 } from '@noble/hashes/sha256';
import { RouteProp, useRoute } from '@react-navigation/native';
import { NativeStackNavigationOptions, NativeStackNavigationProp } from '@react-navigation/native-stack';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import Icon from '../../components/Icon';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
@ -63,10 +63,6 @@ enum ButtonStatus {
type RouteProps = RouteProp<DetailViewStackParamList, 'TransactionStatus'>;
type NavigationProps = NativeStackNavigationProp<DetailViewStackParamList, 'TransactionStatus'>;
type TransactionStatusHeaderOptions = NativeStackNavigationOptions & {
headerTitleContainerStyle?: { flex: number; maxWidth: number };
};
enum ActionType {
SetCPFPPossible,
SetRBFBumpFeePossible,
@ -140,12 +136,8 @@ type TransactionDetailHeaderTitleProps = {
const TransactionDetailHeaderTitle: React.FC<TransactionDetailHeaderTitleProps> = ({ direction, date, directionStyle, dateStyle }) => (
<View style={styles.headerTitleContainer}>
<BlueText style={directionStyle} numberOfLines={1} adjustsFontSizeToFit minimumFontScale={0.8}>
{direction}
</BlueText>
<BlueText style={dateStyle} numberOfLines={2} adjustsFontSizeToFit minimumFontScale={0.8}>
{date}
</BlueText>
<BlueText style={directionStyle}>{direction}</BlueText>
<BlueText style={dateStyle}>{date}</BlueText>
</View>
);
@ -161,57 +153,10 @@ const TransactionStatus: React.FC = () => {
const subscribedWallet = useWalletSubscribe(walletID);
const { navigate, goBack, setOptions } = useExtendedNavigation<NavigationProps>();
const { colors } = useTheme();
const { width: windowWidth, fontScale } = useWindowDimensions();
const { width: windowWidth } = useWindowDimensions();
const { selectedBlockExplorer } = useSettings();
const fetchTxInterval = useRef<NodeJS.Timeout | undefined>(undefined);
const scaledStyles = useMemo(() => {
const valueLineHeight = Math.round(48 * fontScale);
const valuePaddingTop = Math.round(8 * fontScale);
return {
value: {
lineHeight: valueLineHeight,
paddingTop: valuePaddingTop,
minHeight: valueLineHeight + valuePaddingTop,
},
localCurrency: {
lineHeight: Math.round(20 * fontScale),
marginTop: Math.round(6 * fontScale),
},
headerTitleDirection: {
lineHeight: Math.round(22 * fontScale),
},
headerTitleDate: {
lineHeight: Math.round(18 * fontScale),
},
stateLabel: {
lineHeight: Math.round(22 * fontScale),
},
stateValue: {
lineHeight: Math.round(18 * fontScale),
},
advancedHeader: {
minHeight: Math.round(44 * fontScale),
},
explorerButton: {
paddingVertical: Math.round(6 * fontScale),
paddingHorizontal: Math.round(12 * fontScale),
},
addButton: {
paddingVertical: Math.round(4 * fontScale),
paddingHorizontal: Math.round(12 * fontScale),
},
detailRow: {
minHeight: Math.round(24 * fontScale),
paddingVertical: Math.round(12 * fontScale),
},
sectionTitle: {
paddingVertical: Math.round(16 * fontScale),
},
};
}, [fontScale]);
// Explicit width for To/ID text so Android StaticLayout can apply ellipsis (flex alone often fails on Android)
const detailValueMaxWidth = useMemo(() => Math.max(0, Math.floor((windowWidth - 48) / 2)), [windowWidth]);
const detailValueWidthStyle = useMemo(() => ({ width: detailValueMaxWidth }), [detailValueMaxWidth]);
@ -976,20 +921,15 @@ const TransactionStatus: React.FC = () => {
<TransactionDetailHeaderTitle
direction={transactionDirection}
date={transactionDate}
directionStyle={[styles.headerTitleDirection, stylesHook.headerTitleDirection, scaledStyles.headerTitleDirection]}
dateStyle={[styles.headerTitleDate, stylesHook.titleDate, scaledStyles.headerTitleDate]}
directionStyle={[styles.headerTitleDirection, stylesHook.headerTitleDirection]}
dateStyle={[styles.headerTitleDate, stylesHook.titleDate]}
/>
),
headerTitleAlign: 'left',
headerTitleContainerStyle: {
flex: 1,
maxWidth: Math.max(0, windowWidth - 96),
},
} as TransactionStatusHeaderOptions);
});
}
// stylesHook is derived from colors; omitting to avoid unnecessary effect runs
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tx, transactionDirection, transactionDate, setOptions, colors, windowWidth, scaledStyles]);
}, [tx, transactionDirection, transactionDate, setOptions, colors]);
if (loadingError) {
return (
@ -1022,20 +962,15 @@ const TransactionStatus: React.FC = () => {
{/* Value Section */}
<View style={styles.valueCard}>
<View style={styles.valueContent}>
<Text
style={[styles.value, stylesHook.value, scaledStyles.value, styles.valueFullWidth]}
selectable
numberOfLines={1}
adjustsFontSizeToFit
minimumFontScale={0.55}
>
<Text style={[styles.value, stylesHook.value]} selectable numberOfLines={1} adjustsFontSizeToFit minimumFontScale={0.55}>
{txValue !== null ? formatBalanceWithoutSuffix(txValue, preferredBalanceUnit, true) : '-'}
{` `}
{preferredBalanceUnit !== BitcoinUnit.LOCAL_CURRENCY && (
<Text style={[styles.valueUnit, stylesHook.valueUnit]}>{` ${preferredBalanceUnit}`}</Text>
<Text style={[styles.valueUnit, stylesHook.valueUnit]}>{preferredBalanceUnit}</Text>
)}
</Text>
{txValue !== null && (
<Text style={[styles.localCurrency, stylesHook.localCurrency, scaledStyles.localCurrency]}>
<Text style={[styles.localCurrency, stylesHook.localCurrency]}>
{preferredBalanceUnit === BitcoinUnit.LOCAL_CURRENCY
? `${formatBalanceWithoutSuffix(Math.abs(txValue), BitcoinUnit.BTC, true)} ${BitcoinUnit.BTC}`
: satoshiToLocalCurrency(Math.abs(txValue))}
@ -1061,10 +996,8 @@ const TransactionStatus: React.FC = () => {
<View style={styles.stateIndicator}>
<TransactionPendingIcon />
<View style={styles.stateLabelContainer}>
<BlueText style={[styles.stateLabel, stylesHook.stateLabelPending, scaledStyles.stateLabel]}>
{loc.transactions.pending}
</BlueText>
<BlueText style={[styles.stateValue, stylesHook.stateValuePending, styles.stateValueInline, scaledStyles.stateValue]}>
<BlueText style={[styles.stateLabel, stylesHook.stateLabelPending]}>{loc.transactions.pending}</BlueText>
<BlueText style={[styles.stateValue, stylesHook.stateValuePending, styles.stateValueInline]}>
{eta || loc.transactions.details_eta_analyzing}
</BlueText>
</View>
@ -1096,11 +1029,9 @@ const TransactionStatus: React.FC = () => {
<View style={styles.stateIndicator}>
<TransactionOutgoingIcon />
<View style={styles.stateLabelContainer}>
<BlueText style={[styles.stateLabel, stylesHook.stateLabelSent, scaledStyles.stateLabel]}>
{loc.transactions.details_sent}
</BlueText>
<BlueText style={[styles.stateLabel, stylesHook.stateLabelSent]}>{loc.transactions.details_sent}</BlueText>
{isOnChainTx && (
<BlueText style={[styles.stateValue, stylesHook.stateValueSent, styles.stateValueInline, scaledStyles.stateValue]}>
<BlueText style={[styles.stateValue, stylesHook.stateValueSent, styles.stateValueInline]}>
{loc.formatString(loc.transactions.confirmations_lowercase, {
confirmations: parsedConfirmations > 6 ? '6+' : parsedConfirmations,
})}
@ -1112,11 +1043,9 @@ const TransactionStatus: React.FC = () => {
<View style={styles.stateIndicator}>
<TransactionIncomingIcon />
<View style={styles.stateLabelContainer}>
<BlueText style={[styles.stateLabel, stylesHook.stateLabelReceived, scaledStyles.stateLabel]}>
{loc.transactions.details_received}
</BlueText>
<BlueText style={[styles.stateLabel, stylesHook.stateLabelReceived]}>{loc.transactions.details_received}</BlueText>
{isOnChainTx && (
<BlueText style={[styles.stateValue, stylesHook.stateValueReceived, styles.stateValueInline, scaledStyles.stateValue]}>
<BlueText style={[styles.stateValue, stylesHook.stateValueReceived, styles.stateValueInline]}>
{loc.formatString(loc.transactions.confirmations_lowercase, {
confirmations: parsedConfirmations > 6 ? '6+' : parsedConfirmations,
})}
@ -1151,29 +1080,20 @@ const TransactionStatus: React.FC = () => {
{/* Details Section */}
<View style={[styles.detailsCard, stylesHook.detailsCard]}>
{/* Details Title */}
<View style={[styles.sectionTitle, styles.sectionTitleWithButton, stylesHook.sectionTitle, scaledStyles.sectionTitle]}>
<BlueText style={[styles.sectionTitleText, stylesHook.sectionTitleText, styles.sectionTitleTextFlexible]}>
{loc.transactions.details_section}
</BlueText>
<View style={[styles.sectionTitle, styles.sectionTitleWithButton, stylesHook.sectionTitle]}>
<BlueText style={[styles.sectionTitleText, stylesHook.sectionTitleText]}>{loc.transactions.details_section}</BlueText>
{tx?.hash && (
<TouchableOpacity
onPress={handleOpenBlockExplorer}
style={[styles.explorerButton, stylesHook.explorerButton, scaledStyles.explorerButton]}
style={[styles.explorerButton, stylesHook.explorerButton]}
activeOpacity={0.7}
>
<BlueText
style={[styles.explorerButtonText, stylesHook.explorerButtonText]}
numberOfLines={1}
adjustsFontSizeToFit
minimumFontScale={0.8}
>
{loc.transactions.details_explorer}
</BlueText>
<BlueText style={[styles.explorerButtonText, stylesHook.explorerButtonText]}>{loc.transactions.details_explorer}</BlueText>
</TouchableOpacity>
)}
</View>
{/* Network Fee */}
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
<View style={[styles.detailRow, stylesHook.detailRow]}>
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_network_fee}</BlueText>
<View style={styles.detailValueContainer}>
<CopyTextToClipboard
@ -1197,7 +1117,7 @@ const TransactionStatus: React.FC = () => {
const displayText = externalAddresses.map(shortenCounterpartyName).join(', ');
const copyText = externalAddresses.join(', ');
return (
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
<View style={[styles.detailRow, stylesHook.detailRow]}>
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_to_address}</BlueText>
<View style={styles.detailValueContainer}>
<View style={styles.detailValueCopyContainer}>
@ -1223,7 +1143,7 @@ const TransactionStatus: React.FC = () => {
{/* Transaction ID - display shortened so it stays on one line on Android; copy still gets full hash */}
{tx.hash && (
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
<View style={[styles.detailRow, stylesHook.detailRow]}>
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_id}</BlueText>
<View style={styles.detailValueContainer}>
<View style={styles.detailValueCopyContainer}>
@ -1250,7 +1170,7 @@ const TransactionStatus: React.FC = () => {
)}
{/* Note/Memo */}
<View style={[styles.detailRow, styles.detailRowLast, stylesHook.detailRow, scaledStyles.detailRow]}>
<View style={[styles.detailRow, styles.detailRowLast, stylesHook.detailRow]}>
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_note}</BlueText>
<View style={styles.detailValueContainer}>
{memo ? (
@ -1260,19 +1180,8 @@ const TransactionStatus: React.FC = () => {
</BlueText>
</TouchableOpacity>
) : (
<TouchableOpacity
onPress={handleNotePress}
style={[styles.addButton, stylesHook.addButton, scaledStyles.addButton]}
activeOpacity={0.7}
>
<BlueText
style={[styles.addButtonText, stylesHook.addButtonText]}
numberOfLines={1}
adjustsFontSizeToFit
minimumFontScale={0.8}
>
{loc.transactions.details_add_note}
</BlueText>
<TouchableOpacity onPress={handleNotePress} style={[styles.addButton, stylesHook.addButton]} activeOpacity={0.7}>
<BlueText style={[styles.addButtonText, stylesHook.addButtonText]}>{loc.transactions.details_add_note}</BlueText>
</TouchableOpacity>
)}
</View>
@ -1283,13 +1192,11 @@ const TransactionStatus: React.FC = () => {
<View style={[styles.detailsCard, stylesHook.detailsCard]}>
<TouchableOpacity
onPress={() => setIsAdvancedExpanded(!isAdvancedExpanded)}
style={[styles.advancedHeader, stylesHook.advancedHeader, scaledStyles.advancedHeader]}
style={[styles.advancedHeader, stylesHook.advancedHeader]}
activeOpacity={0.85}
>
<View style={[styles.sectionTitle, stylesHook.sectionTitle, styles.sectionTitleRow, scaledStyles.sectionTitle]}>
<BlueText style={[styles.sectionTitleText, stylesHook.sectionTitleText, styles.sectionTitleTextFlexible]} numberOfLines={2}>
{loc.transactions.details_advanced}
</BlueText>
<View style={[styles.sectionTitle, stylesHook.sectionTitle, styles.sectionTitleRow]}>
<BlueText style={[styles.sectionTitleText, stylesHook.sectionTitleText]}>{loc.transactions.details_advanced}</BlueText>
<Icon
name={isAdvancedExpanded ? 'chevron-up' : 'chevron-down'}
type="font-awesome"
@ -1302,7 +1209,7 @@ const TransactionStatus: React.FC = () => {
{isAdvancedExpanded && (
<View style={[styles.advancedContent, stylesHook.advancedContent]}>
{/* Fee Rate */}
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
<View style={[styles.detailRow, stylesHook.detailRow]}>
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_fee_rate}</BlueText>
<View style={styles.detailValueContainer}>
<CopyTextToClipboard
@ -1314,7 +1221,7 @@ const TransactionStatus: React.FC = () => {
</View>
{/* Size */}
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
<View style={[styles.detailRow, stylesHook.detailRow]}>
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_size}</BlueText>
<View style={styles.detailValueContainer}>
<CopyTextToClipboard
@ -1326,7 +1233,7 @@ const TransactionStatus: React.FC = () => {
</View>
{/* Virtual Size */}
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
<View style={[styles.detailRow, stylesHook.detailRow]}>
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_virtual_size}</BlueText>
<View style={styles.detailValueContainer}>
<CopyTextToClipboard
@ -1338,7 +1245,7 @@ const TransactionStatus: React.FC = () => {
</View>
{/* Transaction Hex */}
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
<View style={[styles.detailRow, stylesHook.detailRow]}>
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_tx_hex}</BlueText>
<View style={styles.detailValueContainer}>
{txHex ? (
@ -1403,7 +1310,6 @@ const styles = StyleSheet.create({
alignItems: 'flex-start',
justifyContent: 'center',
flex: 1,
minWidth: 0,
},
headerTitleDirection: {
fontSize: 17,
@ -1451,20 +1357,15 @@ const styles = StyleSheet.create({
alignItems: 'flex-start',
justifyContent: 'flex-start',
overflow: 'visible',
width: '100%',
},
value: {
fontSize: 40,
fontWeight: '700',
letterSpacing: -0.5,
lineHeight: 48,
lineHeight: 32,
paddingTop: 8,
minHeight: 38,
},
valueFullWidth: {
width: '100%',
flexShrink: 1,
},
valueUnit: {
fontSize: 18,
fontWeight: '600',
@ -1482,6 +1383,7 @@ const styles = StyleSheet.create({
borderRadius: 12,
marginHorizontal: 24,
marginBottom: 42,
overflow: 'hidden',
},
stateSection: {
alignItems: 'flex-start',
@ -1499,7 +1401,6 @@ const styles = StyleSheet.create({
alignItems: 'flex-start',
marginLeft: 8,
flex: 1,
minWidth: 0,
},
stateLabel: {
fontSize: 16,
@ -1585,23 +1486,17 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8,
},
sectionTitleText: {
fontSize: 17,
fontWeight: '600',
},
sectionTitleTextFlexible: {
flex: 1,
flexShrink: 1,
minWidth: 0,
},
explorerButton: {
paddingVertical: 6,
paddingHorizontal: 12,
borderRadius: 6,
alignSelf: 'flex-end',
flexShrink: 0,
minWidth: 50,
alignItems: 'center',
justifyContent: 'center',
},
@ -1612,7 +1507,7 @@ const styles = StyleSheet.create({
detailRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
alignItems: 'center',
marginBottom: 0,
minHeight: 24,
paddingVertical: 12,
@ -1636,8 +1531,6 @@ const styles = StyleSheet.create({
fontSize: 16,
fontWeight: '500',
flex: 1,
flexShrink: 1,
minWidth: 0,
lineHeight: 22,
paddingRight: 12,
},
@ -1651,12 +1544,11 @@ const styles = StyleSheet.create({
flex: 1,
minWidth: 0,
maxWidth: '100%',
flexWrap: 'wrap',
alignItems: 'flex-end',
flexWrap: 'nowrap',
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'flex-end',
gap: 8,
flexShrink: 0,
},
detailValueCopyContainer: {
flex: 1,
@ -1704,7 +1596,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 12,
borderRadius: 6,
alignSelf: 'flex-end',
flexShrink: 0,
minWidth: 50,
alignItems: 'center',
justifyContent: 'center',
},
@ -1722,6 +1614,7 @@ const styles = StyleSheet.create({
borderWidth: 1,
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
overflow: 'hidden',
},
advancedContent: {
marginTop: 0,

View File

@ -33,7 +33,6 @@ import presentAlert, { AlertType } from '../../components/Alert';
import { FButton, FContainer, FloatButtonsBottomFade } from '../../components/FloatButtons';
import { useTheme } from '../../components/themes';
import { TransactionListItem } from '../../components/TransactionListItem';
import { TX_ROW_BASE_HEIGHT } from '../../components/ListItem';
import TransactionsNavigationHeader, { actionKeys } from '../../components/TransactionsNavigationHeader';
import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
@ -438,17 +437,11 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
[name, navigate, navigation, onWalletSelect, walletID, wallets],
);
const { fontScale } = useWindowDimensions();
const txRowHeight = Math.round(TX_ROW_BASE_HEIGHT * fontScale);
const getItemLayout = useCallback(
(_: any, index: number) => ({
length: txRowHeight,
offset: txRowHeight * index,
index,
}),
[txRowHeight],
);
const getItemLayout = (_: any, index: number) => ({
length: 64,
offset: 64 * index,
index,
});
const renderItem = useCallback(
// react/no-unused-prop-types misfires on inline arrow renderers: it reads the

View File

@ -8,15 +8,10 @@ import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/h
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import { ExtendedTransaction, Transaction, TWallet } from '../../class/wallets/types';
import presentAlert from '../../components/Alert';
import { FButton, FContainer, FloatButtonsBottomFade, getFloatingButtonReservedHeight } from '../../components/FloatButtons';
import { FButton, FContainer, FloatButtonsBottomFade } from '../../components/FloatButtons';
import { useTheme } from '../../components/themes';
import { TransactionListItem } from '../../components/TransactionListItem';
import { TX_ROW_BASE_HEIGHT } from '../../components/ListItem';
import WalletsCarousel, {
getWalletCarouselItemWidth,
CarouselListRefType,
getWalletCarouselHeight,
} from '../../components/WalletsCarousel';
import WalletsCarousel, { getWalletCarouselItemWidth, CarouselListRefType } from '../../components/WalletsCarousel';
import { useSizeClass, SizeClass } from '../../blue_modules/sizeClass';
import loc from '../../loc';
import ActionSheet from '../ActionSheet';
@ -33,7 +28,6 @@ import { scanQrHelper } from '../../helpers/scan-qr';
import { isIOS26OrHigher } from '../../components/platform';
const WalletsListSections = { CAROUSEL: 'CAROUSEL', TRANSACTIONS: 'TRANSACTIONS' };
const SECTION_HEADER_BASE_HEIGHT = 56;
/** Electrum `ping` while the list is visible; detects mid-session drops without polling when user is elsewhere. */
const ELECTRUM_HEALTH_POLL_WHILE_WALLETS_LIST_FOCUSED_MS = 30_000;
@ -114,11 +108,7 @@ const WalletsList: React.FC = () => {
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
const { wallets, getTransactions, refreshAllWalletTransactions } = useStorage();
const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings();
const { width, fontScale } = useWindowDimensions();
const carouselHeight = getWalletCarouselHeight(fontScale);
const transactionItemHeight = Math.round(TX_ROW_BASE_HEIGHT * fontScale);
const sectionHeaderHeight = Math.round(SECTION_HEADER_BASE_HEIGHT * fontScale);
const floatingButtonHeight = getFloatingButtonReservedHeight(fontScale);
const { width } = useWindowDimensions();
const { colors, scanImage } = useTheme();
const navigation = useExtendedNavigation<NavigationProps>();
const isFocused = useIsFocused();
@ -134,11 +124,9 @@ const WalletsList: React.FC = () => {
listHeaderBack: {
backgroundColor: colors.background,
paddingTop: sizeClass === SizeClass.Large ? 8 : 0,
minHeight: sectionHeaderHeight,
},
listHeaderText: {
color: colors.foregroundColor,
marginVertical: Math.round(16 * fontScale),
},
});
@ -505,9 +493,14 @@ const WalletsList: React.FC = () => {
}, [sizeClass, dataSource]);
// Constants for layout calculations
const TRANSACTION_ITEM_HEIGHT = 80;
const CAROUSEL_HEIGHT = 195;
const SECTION_HEADER_HEIGHT = 56; // Base height
const LARGE_TITLE_EXTRA_HEIGHT = 20; // Additional height for large titles
const getSectionHeaderHeight = useCallback(() => {
return sectionHeaderHeight + (sizeClass === SizeClass.Large ? Math.round(20 * fontScale) : 0);
}, [sizeClass, sectionHeaderHeight, fontScale]);
return SECTION_HEADER_HEIGHT + (sizeClass === SizeClass.Large ? LARGE_TITLE_EXTRA_HEIGHT : 0);
}, [sizeClass]);
const getItemLayout = useCallback(
(data: any, index: number) => {
@ -516,8 +509,8 @@ const WalletsList: React.FC = () => {
if (sizeClass === SizeClass.Large) {
// On large screens: only transaction items, no carousel
return {
length: transactionItemHeight,
offset: transactionItemHeight * index,
length: TRANSACTION_ITEM_HEIGHT,
offset: TRANSACTION_ITEM_HEIGHT * index,
index,
};
} else {
@ -525,7 +518,7 @@ const WalletsList: React.FC = () => {
// First section: Carousel
if (index === 0) {
return {
length: carouselHeight,
length: CAROUSEL_HEIGHT,
offset: 0,
index,
};
@ -538,13 +531,13 @@ const WalletsList: React.FC = () => {
// 3. Transaction items
const transactionIndex = index - 1; // Adjust index to account for carousel
return {
length: transactionItemHeight,
offset: carouselHeight + headerHeight + transactionItemHeight * transactionIndex,
length: TRANSACTION_ITEM_HEIGHT,
offset: CAROUSEL_HEIGHT + headerHeight + TRANSACTION_ITEM_HEIGHT * transactionIndex,
index,
};
}
},
[sizeClass, getSectionHeaderHeight, carouselHeight, transactionItemHeight],
[sizeClass, getSectionHeaderHeight],
);
return (
@ -557,7 +550,7 @@ const WalletsList: React.FC = () => {
initialNumToRender={10}
renderSectionFooter={renderSectionFooter}
sections={sections}
floatingButtonHeight={floatingButtonHeight}
floatingButtonHeight={70}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
getItemLayout={getItemLayout}

View File

@ -1,6 +1,6 @@
import assert from 'assert';
import { evpBytesToKeyMd5 } from '../../blue_modules/encryption';
import { evpBytesToKeyMd5 } from '../../blue_modules/crypto/evp_bytes_to_key';
import { hexToUint8Array, stringToUint8Array, uint8ArrayToHex } from '../../blue_modules/uint8array-extras';
describe('evpBytesToKeyMd5', () => {