Compare commits
2 Commits
master
...
renovate/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a50b50496f | ||
|
|
6ac5710c56 |
2
Gemfile
2
Gemfile
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,98 +1,23 @@
|
||||
import { cbc } from '@noble/ciphers/aes';
|
||||
import { md5 } from '@noble/hashes/legacy';
|
||||
import { randomBytes } from '@noble/hashes/utils';
|
||||
import AES from 'crypto-js/aes';
|
||||
import Utf8 from 'crypto-js/enc-utf8';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// "Salted__" — OpenSSL envelope magic. Hardcoded as bytes so the wire
|
||||
// format cannot drift through any encoder.
|
||||
const SALT_MAGIC = new Uint8Array([0x53, 0x61, 0x6c, 0x74, 0x65, 0x64, 0x5f, 0x5f]);
|
||||
const SALT_LEN = 8;
|
||||
const KEY_LEN = 32;
|
||||
const IV_LEN = 16;
|
||||
const BLOCK_LEN = 16;
|
||||
|
||||
/**
|
||||
* AES-256-CBC encrypt with the OpenSSL "Salted__" envelope, EVP_BytesToKey-MD5
|
||||
* key derivation and PKCS7 padding. Output is base64-encoded.
|
||||
*
|
||||
* Wire format is bit-identical to CryptoJS@4.x's default
|
||||
* `AES.encrypt(data, password).toString()` — we kept the swap-the-library
|
||||
* change a drop-in replacement so existing encrypted wallets on user
|
||||
* devices remain readable, with no migration step.
|
||||
*/
|
||||
export function encrypt(data: string, password: string): string {
|
||||
if (data.length < 10) throw new Error('data length cant be < 10');
|
||||
const salt = randomBytes(SALT_LEN);
|
||||
const kdf = evpBytesToKeyMd5(stringToUint8Array(password), salt, KEY_LEN + IV_LEN);
|
||||
const key = kdf.subarray(0, KEY_LEN);
|
||||
const iv = kdf.subarray(KEY_LEN);
|
||||
const ciphertext = cbc(key, iv).encrypt(stringToUint8Array(data));
|
||||
return uint8ArrayToBase64(concatUint8Arrays([SALT_MAGIC, salt, ciphertext]));
|
||||
const ciphertext = AES.encrypt(data, password);
|
||||
return ciphertext.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverse of `encrypt`. Accepts the legacy CryptoJS wire format and returns
|
||||
* the original UTF-8 plaintext. Any error (bad base64, missing magic, wrong
|
||||
* password, bad padding) collapses to `false`.
|
||||
*/
|
||||
export function decrypt(data: string, password: string): string | false {
|
||||
const bytes = AES.decrypt(data, password);
|
||||
let str: string | false = false;
|
||||
try {
|
||||
// crypto-js's base64 decoder ignored whitespace. Some old encrypted-backup
|
||||
// export/import flows (manual file paste, clipboard transit, email-based
|
||||
// wallet transfer) introduced stray newlines or padding spaces. Strip them
|
||||
// before strict base64 decode so legacy backups still open. `\s` does not
|
||||
// include `=`, so base64 padding survives.
|
||||
const envelope = base64ToUint8Array(data.replace(/\s+/g, ''));
|
||||
if (envelope.length < SALT_MAGIC.length + SALT_LEN + BLOCK_LEN) return false;
|
||||
if (!areUint8ArraysEqual(envelope.subarray(0, SALT_MAGIC.length), SALT_MAGIC)) return false;
|
||||
const salt = envelope.subarray(SALT_MAGIC.length, SALT_MAGIC.length + SALT_LEN);
|
||||
const ciphertext = envelope.subarray(SALT_MAGIC.length + SALT_LEN);
|
||||
const kdf = evpBytesToKeyMd5(stringToUint8Array(password), salt, KEY_LEN + IV_LEN);
|
||||
const key = kdf.subarray(0, KEY_LEN);
|
||||
const iv = kdf.subarray(KEY_LEN);
|
||||
const plain = cbc(key, iv).decrypt(ciphertext);
|
||||
// Strict UTF-8 decode — wrong-password decrypts that happen to survive
|
||||
// PKCS7 unpadding overwhelmingly fail here (crypto-js's `enc.Utf8` was
|
||||
// strict too; we preserve that gate by using `fatal: true`).
|
||||
const str = new TextDecoder('utf-8', { fatal: true }).decode(plain);
|
||||
// Belt-and-suspenders: legitimate plaintext is always ≥ 10 chars
|
||||
// (enforced by encrypt()), so anything shorter is rejected.
|
||||
if (str.length < 10) return false;
|
||||
return str;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
str = bytes.toString(Utf8);
|
||||
} catch (e) {}
|
||||
|
||||
// For some reason, sometimes decrypt would succeed with an incorrect password and return random characters.
|
||||
// In this TypeScript version, we are not allowing the encryption of data that is shorter than
|
||||
// 10 characters. If the decrypted data is less than 10 characters, we assume that the decrypt actually failed.
|
||||
if (str && str.length < 10) return false;
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { bech32 } from 'bech32';
|
||||
import bolt11 from 'bolt11';
|
||||
import { sha256 } from '@noble/hashes/sha256';
|
||||
import { hmac } from '@noble/hashes/hmac';
|
||||
import { cbc } from '@noble/ciphers/aes';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import ecc from '../blue_modules/noble_ecc';
|
||||
import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api
|
||||
import { fetch } from '../util/fetch';
|
||||
@ -321,24 +321,13 @@ export default class Lnurl {
|
||||
}
|
||||
|
||||
static decipherAES(ciphertextBase64: string, preimageHex: string, ivBase64: string): string {
|
||||
// crypto-js's old implementation silently returned '' on malformed
|
||||
// ciphertext (non-16-aligned bytes, bad PKCS7 padding) and threw on
|
||||
// malformed UTF-8 plaintext. @noble/ciphers throws on the former. We
|
||||
// catch every throw and return '' — the call site at
|
||||
// screen/lnd/lnurlPaySuccess.tsx renders this directly without a
|
||||
// try/catch, so a misbehaving LNURL server should not crash the screen.
|
||||
// Note: unlike crypto-js's strict `enc.Utf8` decoder, `uint8ArrayToString`
|
||||
// is lenient on bad UTF-8 (mojibake instead of throw); this is strictly
|
||||
// safer than the old behaviour for this user-facing path.
|
||||
try {
|
||||
const key = hexToUint8Array(preimageHex);
|
||||
const iv = base64ToUint8Array(ivBase64);
|
||||
const ct = base64ToUint8Array(ciphertextBase64);
|
||||
const pt = cbc(key, iv).decrypt(ct);
|
||||
return uint8ArrayToString(pt);
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
const iv = CryptoJS.enc.Base64.parse(ivBase64);
|
||||
const key = CryptoJS.enc.Hex.parse(preimageHex);
|
||||
return CryptoJS.AES.decrypt(uint8ArrayToHex(base64ToUint8Array(ciphertextBase64)), key, {
|
||||
iv,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
format: CryptoJS.format.Hex,
|
||||
}).toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
|
||||
getCommentAllowed(): number | false {
|
||||
|
||||
@ -106,31 +106,23 @@ export class MultisigCosigner {
|
||||
this._valid = false;
|
||||
}
|
||||
|
||||
// is it coldcard / unchained json?
|
||||
// is it coldcard json?
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
|
||||
// p2wsh_p2sh (Coldcard), p2sh_p2wsh (Unchained)
|
||||
// same script type with reversed naming
|
||||
const xpub = json.p2wsh_p2sh || json.p2sh_p2wsh;
|
||||
const path = (json.p2wsh_p2sh_deriv || json.p2sh_p2wsh_deriv)?.replace(/h/g, "'");
|
||||
const p2sh_deriv = json.p2sh_deriv?.replace(/h/g, "'");
|
||||
const p2wsh_deriv = json.p2wsh_deriv?.replace(/h/g, "'");
|
||||
|
||||
if (json.p2sh && p2sh_deriv && json.xfp) {
|
||||
const cc = new MultisigCosigner(MultisigCosigner.exportToJson(json.xfp, json.p2sh, p2sh_deriv));
|
||||
if (json.p2sh && json.p2sh_deriv && json.xfp) {
|
||||
const cc = new MultisigCosigner(MultisigCosigner.exportToJson(json.xfp, json.p2sh, json.p2sh_deriv));
|
||||
this._valid = true;
|
||||
this._cosigners.push(cc);
|
||||
}
|
||||
|
||||
if (xpub && path && json.xfp) {
|
||||
const cc = new MultisigCosigner(MultisigCosigner.exportToJson(json.xfp, xpub, path));
|
||||
if (json.p2wsh_p2sh && json.p2wsh_p2sh_deriv && json.xfp) {
|
||||
const cc = new MultisigCosigner(MultisigCosigner.exportToJson(json.xfp, json.p2wsh_p2sh, json.p2wsh_p2sh_deriv));
|
||||
this._valid = true;
|
||||
this._cosigners.push(cc);
|
||||
}
|
||||
|
||||
if (json.p2wsh && p2wsh_deriv && json.xfp) {
|
||||
const cc = new MultisigCosigner(MultisigCosigner.exportToJson(json.xfp, json.p2wsh, p2wsh_deriv));
|
||||
if (json.p2wsh && json.p2wsh_deriv && json.xfp) {
|
||||
const cc = new MultisigCosigner(MultisigCosigner.exportToJson(json.xfp, json.p2wsh, json.p2wsh_deriv));
|
||||
this._valid = true;
|
||||
this._cosigners.push(cc);
|
||||
}
|
||||
|
||||
@ -658,12 +658,6 @@ export class LightningArkWallet extends LightningCustodianWallet {
|
||||
|
||||
const paymentResult = await this._arkadeSwaps.sendLightningPayment({ invoice });
|
||||
|
||||
this.last_paid_invoice_result = {
|
||||
payment_preimage: paymentResult.preimage,
|
||||
payment_hash: invoiceDetails.paymentHash,
|
||||
payment_request: invoice,
|
||||
};
|
||||
|
||||
console.log('Payment successful!');
|
||||
console.log('Amount:', paymentResult.amount);
|
||||
console.log('Preimage:', paymentResult.preimage);
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -7,18 +7,10 @@ import { useTheme } from './themes';
|
||||
interface SafeAreaScrollViewProps extends ScrollViewProps {
|
||||
floatingButtonHeight?: number;
|
||||
headerHeight?: number; // Additional header height to account for (e.g., when headerTransparent is true)
|
||||
disableDefaultTopPadding?: boolean;
|
||||
}
|
||||
|
||||
const SafeAreaScrollView = forwardRef<ScrollView, SafeAreaScrollViewProps>((props, ref) => {
|
||||
const {
|
||||
style,
|
||||
contentContainerStyle,
|
||||
floatingButtonHeight = 0,
|
||||
headerHeight = 0,
|
||||
disableDefaultTopPadding = false,
|
||||
...otherProps
|
||||
} = props;
|
||||
const { style, contentContainerStyle, floatingButtonHeight = 0, headerHeight = 0, ...otherProps } = props;
|
||||
const { colors } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
@ -40,10 +32,7 @@ const SafeAreaScrollView = forwardRef<ScrollView, SafeAreaScrollViewProps>((prop
|
||||
if (headerHeight > 0) {
|
||||
return headerHeight;
|
||||
}
|
||||
if (disableDefaultTopPadding) {
|
||||
return 0;
|
||||
}
|
||||
// Preserve legacy behavior for existing screens
|
||||
// iOS safe area or no status bar
|
||||
return insets.top > 0 ? 5 : 0;
|
||||
})(),
|
||||
};
|
||||
@ -59,7 +48,7 @@ const SafeAreaScrollView = forwardRef<ScrollView, SafeAreaScrollViewProps>((prop
|
||||
|
||||
// Now compose with contentContainerStyle to ensure passed styles override defaults
|
||||
return StyleSheet.compose(basePadding, contentContainerStyle);
|
||||
}, [insets, contentContainerStyle, floatingButtonHeight, headerHeight, disableDefaultTopPadding]);
|
||||
}, [insets, contentContainerStyle, floatingButtonHeight, headerHeight]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import { Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import Animated, { useAnimatedStyle, useSharedValue, withSpring, withTiming } from 'react-native-reanimated';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import { useTheme } from './themes';
|
||||
import { LightningArkWallet } from '../class/wallets/lightning-ark-wallet';
|
||||
import { LightningCustodianWallet } from '../class/wallets/lightning-custodian-wallet';
|
||||
import { MultisigHDWallet } from '../class/wallets/multisig-hd-wallet';
|
||||
@ -14,39 +14,35 @@ import { FiatUnit } from '../models/fiatUnit';
|
||||
import { BlurredBalanceView } from './BlurredBalanceView';
|
||||
import { useSettings } from '../hooks/context/useSettings';
|
||||
import ToolTipMenu from './TooltipMenu';
|
||||
import useAnimateOnChange from '../hooks/useAnimateOnChange';
|
||||
import { useLocale } from '@react-navigation/native';
|
||||
import ActionSheet from '../screen/ActionSheet';
|
||||
|
||||
const HERO_BASE_BODY_MIN_HEIGHT = 120;
|
||||
const HERO_MIN_BODY_HEIGHT = Math.round(HERO_BASE_BODY_MIN_HEIGHT * 1.2);
|
||||
const HERO_BOTTOM_PADDING = 32;
|
||||
const WALLET_LABEL_TOP_GAP = 32;
|
||||
|
||||
interface TransactionsNavigationHeaderProps {
|
||||
wallet: TWallet;
|
||||
unit: BitcoinUnit;
|
||||
headerOverlayHeight: number;
|
||||
onWalletUnitChange: (unit: BitcoinUnit) => void;
|
||||
onManageFundsPressed?: (id?: string) => void;
|
||||
onWalletBalanceVisibilityChange?: (shouldHideBalance: boolean) => void;
|
||||
onWalletBalanceVisibilityChange?: (isShouldBeVisible: boolean) => void;
|
||||
unitSwitching?: boolean;
|
||||
}
|
||||
|
||||
const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps> = ({
|
||||
wallet,
|
||||
headerOverlayHeight,
|
||||
onWalletUnitChange,
|
||||
onManageFundsPressed,
|
||||
onWalletBalanceVisibilityChange,
|
||||
unit = BitcoinUnit.BTC,
|
||||
unitSwitching = false,
|
||||
}) => {
|
||||
const { colors } = useTheme();
|
||||
const { hideBalance } = wallet;
|
||||
const isLightningWallet = wallet.type === LightningCustodianWallet.type || wallet.type === LightningArkWallet.type;
|
||||
const [allowOnchainAddress, setAllowOnchainAddress] = useState(isLightningWallet);
|
||||
const { preferredFiatCurrency } = useSettings();
|
||||
const { direction } = useLocale();
|
||||
const balanceOpacity = useSharedValue(1);
|
||||
const balanceTranslateY = useSharedValue(0);
|
||||
const previousBalance = useRef<string | undefined>(undefined);
|
||||
|
||||
const verifyIfWalletAllowsOnchainAddress = useCallback(() => {
|
||||
if (isLightningWallet) {
|
||||
@ -77,14 +73,13 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
||||
|
||||
const handleBalanceVisibility = useCallback(() => {
|
||||
onWalletBalanceVisibilityChange?.(!hideBalance);
|
||||
}, [hideBalance, onWalletBalanceVisibilityChange]);
|
||||
}, [onWalletBalanceVisibilityChange, hideBalance]);
|
||||
|
||||
const changeWalletBalanceUnit = () => {
|
||||
if (hideBalance) {
|
||||
return;
|
||||
}
|
||||
let newWalletPreferredUnit = wallet.getPreferredBalanceUnit();
|
||||
|
||||
console.debug('[UnitSwitch/UI] tap unit change', { walletID: wallet.getID?.(), current: newWalletPreferredUnit });
|
||||
|
||||
if (newWalletPreferredUnit === BitcoinUnit.BTC) {
|
||||
newWalletPreferredUnit = BitcoinUnit.SATS;
|
||||
} else if (newWalletPreferredUnit === BitcoinUnit.SATS) {
|
||||
@ -93,6 +88,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
||||
newWalletPreferredUnit = BitcoinUnit.BTC;
|
||||
}
|
||||
|
||||
console.debug('[UnitSwitch/UI] next unit resolved', { walletID: wallet.getID?.(), next: newWalletPreferredUnit });
|
||||
onWalletUnitChange(newWalletPreferredUnit);
|
||||
};
|
||||
|
||||
@ -107,9 +103,9 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
||||
|
||||
const onPressMenuItem = useCallback(
|
||||
(id: string) => {
|
||||
if (id === actionKeys.WalletBalanceVisibility) {
|
||||
if (id === 'walletBalanceVisibility') {
|
||||
handleBalanceVisibility();
|
||||
} else if (id === actionKeys.CopyToClipboard) {
|
||||
} else if (id === 'copyToClipboard') {
|
||||
handleCopyPress();
|
||||
}
|
||||
},
|
||||
@ -144,160 +140,148 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
||||
}, [unit, currentBalance]);
|
||||
|
||||
const balance = !wallet.hideBalance && formattedBalance;
|
||||
const safeBalance = balance ? String(balance) : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (hideBalance) {
|
||||
previousBalance.current = undefined;
|
||||
balanceOpacity.value = 1;
|
||||
balanceTranslateY.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousBalance.current !== undefined && previousBalance.current !== safeBalance) {
|
||||
balanceOpacity.value = 0;
|
||||
balanceTranslateY.value = 6;
|
||||
balanceOpacity.value = withTiming(1, { duration: 180 });
|
||||
balanceTranslateY.value = withSpring(0, { damping: 16, stiffness: 220 });
|
||||
}
|
||||
|
||||
previousBalance.current = safeBalance;
|
||||
}, [safeBalance, hideBalance, balanceOpacity, balanceTranslateY]);
|
||||
|
||||
const balanceAnimationKey = useMemo(
|
||||
() => `${wallet.getID?.() ?? ''}-${unit}-${hideBalance}-${safeBalance ?? ''}`,
|
||||
[safeBalance, hideBalance, unit, wallet],
|
||||
);
|
||||
const balanceAnimatedStyle = useAnimateOnChange(balanceAnimationKey);
|
||||
|
||||
const animatedBalanceTextStyle = useAnimatedStyle(() => ({
|
||||
opacity: balanceOpacity.value,
|
||||
transform: [{ translateY: balanceTranslateY.value }],
|
||||
}));
|
||||
|
||||
const toolTipWalletBalanceActions = useMemo(() => {
|
||||
return hideBalance
|
||||
? [
|
||||
{
|
||||
id: actionKeys.WalletBalanceVisibility,
|
||||
id: 'walletBalanceVisibility',
|
||||
text: loc.transactions.details_balance_show,
|
||||
icon: actionIcons.Eye,
|
||||
icon: {
|
||||
iconValue: 'eye',
|
||||
},
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: actionKeys.WalletBalanceVisibility,
|
||||
id: 'walletBalanceVisibility',
|
||||
text: loc.transactions.details_balance_hide,
|
||||
icon: actionIcons.EyeSlash,
|
||||
icon: {
|
||||
iconValue: 'eye.slash',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: actionKeys.CopyToClipboard,
|
||||
id: 'copyToClipboard',
|
||||
text: loc.transactions.details_copy,
|
||||
icon: actionIcons.Clipboard,
|
||||
icon: {
|
||||
iconValue: 'doc.on.doc',
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [hideBalance]);
|
||||
|
||||
useEffect(() => {
|
||||
console.debug('[UnitSwitch/UI] render state', {
|
||||
walletID: wallet.getID?.(),
|
||||
unit,
|
||||
hideBalance,
|
||||
preferredFiat: preferredFiatCurrency?.endPointKey,
|
||||
switching: unitSwitching,
|
||||
});
|
||||
}, [wallet, unit, hideBalance, preferredFiatCurrency, unitSwitching]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.lineaderGradient,
|
||||
{
|
||||
paddingTop: headerOverlayHeight,
|
||||
minHeight: headerOverlayHeight + HERO_MIN_BODY_HEIGHT,
|
||||
backgroundColor: WalletGradient.headerColorFor(wallet.type),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<LinearGradient colors={WalletGradient.gradientsFor(wallet.type)} style={StyleSheet.absoluteFill} />
|
||||
<LinearGradient colors={WalletGradient.gradientsFor(wallet.type)} style={styles.lineaderGradient}>
|
||||
<View style={styles.contentContainer}>
|
||||
<Text testID="WalletLabel" numberOfLines={1} style={[styles.walletLabel, { writingDirection: direction }]}>
|
||||
{wallet.getLabel()}
|
||||
</Text>
|
||||
<View style={styles.balanceSection}>
|
||||
<View style={styles.walletBalanceAndUnitContainer}>
|
||||
<ToolTipMenu
|
||||
shouldOpenOnLongPress
|
||||
isButton
|
||||
enableAndroidRipple={false}
|
||||
buttonStyle={styles.walletBalance}
|
||||
onPressMenuItem={onPressMenuItem}
|
||||
actions={toolTipWalletBalanceActions}
|
||||
>
|
||||
<View style={styles.walletBalance}>
|
||||
{hideBalance ? (
|
||||
<BlurredBalanceView />
|
||||
) : (
|
||||
<Text
|
||||
<Animated.View style={[styles.walletBalanceAndUnitContainer, balanceAnimatedStyle]}>
|
||||
<ToolTipMenu
|
||||
shouldOpenOnLongPress
|
||||
isButton
|
||||
enableAndroidRipple={false}
|
||||
buttonStyle={styles.walletBalance}
|
||||
onPressMenuItem={onPressMenuItem}
|
||||
actions={toolTipWalletBalanceActions}
|
||||
>
|
||||
<View style={styles.walletBalance}>
|
||||
{hideBalance ? (
|
||||
<BlurredBalanceView />
|
||||
) : (
|
||||
<View key={`wallet-balance-textwrap-${wallet.getID?.() ?? ''}-${String(balance)}`}>
|
||||
<Animated.Text
|
||||
key={`wallet-balance-text-${wallet.getID?.() ?? ''}-${String(balance)}`} // force recreation on balance change for RTL correctness
|
||||
testID="WalletBalance"
|
||||
numberOfLines={1}
|
||||
minimumFontScale={0.5}
|
||||
adjustsFontSizeToFit
|
||||
style={styles.walletBalanceText}
|
||||
style={[styles.walletBalanceText, animatedBalanceTextStyle]}
|
||||
>
|
||||
{balance}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</ToolTipMenu>
|
||||
{!hideBalance && (
|
||||
<TouchableOpacity style={styles.walletPreferredUnitView} onPress={changeWalletBalanceUnit} disabled={unitSwitching}>
|
||||
<Text style={styles.walletPreferredUnitText}>
|
||||
{unit === BitcoinUnit.LOCAL_CURRENCY ? (preferredFiatCurrency?.endPointKey ?? FiatUnit.USD) : unit}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
{(wallet.type === LightningCustodianWallet.type || wallet.type === LightningArkWallet.type) && allowOnchainAddress && (
|
||||
<TouchableOpacity style={styles.manageFundsButton} accessibilityRole="button" onPress={showManageFundsActionSheet}>
|
||||
<Text style={styles.manageFundsButtonText}>{loc.lnd.title}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</Animated.Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</ToolTipMenu>
|
||||
<TouchableOpacity style={styles.walletPreferredUnitView} onPress={changeWalletBalanceUnit} disabled={unitSwitching}>
|
||||
<Text style={styles.walletPreferredUnitText}>
|
||||
{unit === BitcoinUnit.LOCAL_CURRENCY ? (preferredFiatCurrency?.endPointKey ?? FiatUnit.USD) : unit}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
{(wallet.type === LightningCustodianWallet.type || wallet.type === LightningArkWallet.type) && allowOnchainAddress && (
|
||||
<TouchableOpacity style={styles.manageFundsButton} accessibilityRole="button" onPress={showManageFundsActionSheet}>
|
||||
<Text style={styles.manageFundsButtonText}>{loc.lnd.title}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{wallet.type === MultisigHDWallet.type && (
|
||||
<TouchableOpacity style={styles.manageFundsButton} accessibilityRole="button" onPress={() => handleManageFundsPressed()}>
|
||||
<Text style={styles.manageFundsButtonText}>{loc.multisig.manage_keys}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.bottomBarSpacer}>
|
||||
<View
|
||||
style={[
|
||||
styles.bottomBar,
|
||||
{
|
||||
backgroundColor: colors.background,
|
||||
...Platform.select({
|
||||
ios: { shadowColor: colors.shadowColor },
|
||||
android: {},
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
lineaderGradient: {
|
||||
minHeight: 140,
|
||||
justifyContent: 'flex-start',
|
||||
position: 'relative',
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
paddingTop: WALLET_LABEL_TOP_GAP,
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: HERO_BOTTOM_PADDING,
|
||||
},
|
||||
bottomBarSpacer: {
|
||||
position: 'relative',
|
||||
height: 12,
|
||||
marginBottom: 0,
|
||||
},
|
||||
bottomBar: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: -1,
|
||||
height: 13,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
shadowOffset: { width: 0, height: -8 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 6,
|
||||
},
|
||||
android: {
|
||||
elevation: 0.5,
|
||||
},
|
||||
}),
|
||||
padding: 15,
|
||||
},
|
||||
walletLabel: {
|
||||
backgroundColor: 'transparent',
|
||||
fontSize: 19,
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
marginBottom: 4,
|
||||
color: '#fff',
|
||||
marginBottom: 10,
|
||||
},
|
||||
walletBalance: {
|
||||
flexShrink: 1,
|
||||
marginRight: 6,
|
||||
minHeight: 39,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
balanceSection: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
manageFundsButton: {
|
||||
marginTop: 14,
|
||||
@ -318,13 +302,13 @@ const styles = StyleSheet.create({
|
||||
walletBalanceAndUnitContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingRight: 10,
|
||||
paddingRight: 10, // Ensure there's some padding to the right
|
||||
},
|
||||
walletBalanceText: {
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 36,
|
||||
flexShrink: 1,
|
||||
flexShrink: 1, // Allow the text to shrink if there's not enough space
|
||||
},
|
||||
walletPreferredUnitView: {
|
||||
justifyContent: 'center',
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -31,8 +31,6 @@ export { platformColors } from '../themes';
|
||||
|
||||
export const isAndroid = Platform.OS === 'android';
|
||||
const isIOS = Platform.OS === 'ios';
|
||||
const iosMajorVersion = isIOS ? Number(String(Platform.Version).split('.')[0]) : 0;
|
||||
export const isIOS26OrHigher = isIOS && Number.isFinite(iosMajorVersion) && iosMajorVersion >= 26;
|
||||
|
||||
export const platformSizing = {
|
||||
horizontalPadding: isIOS ? 16 : 20,
|
||||
@ -109,15 +107,6 @@ export const getSettingsHeaderOptions = (
|
||||
const cardColor = colors.lightButton ?? colors.modal ?? colors.elevated ?? defaultBackgroundColor;
|
||||
const headerBackgroundColor = isIOS ? (dark ? defaultBackgroundColor : cardColor) : defaultBackgroundColor;
|
||||
|
||||
if (isIOS26OrHigher) {
|
||||
return {
|
||||
title,
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleShadowVisible: true,
|
||||
headerBackButtonDisplayMode: 'minimal' as const,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
headerLargeTitle: isIOS,
|
||||
@ -203,7 +192,6 @@ export const SettingsScrollView = forwardRef<ScrollView, SettingsScrollViewProps
|
||||
ref={ref}
|
||||
style={[style, { backgroundColor: screenBackgroundColor }]}
|
||||
headerHeight={resolvedHeaderHeight}
|
||||
disableDefaultTopPadding={isIOS26OrHigher}
|
||||
floatingButtonHeight={floatingButtonHeight}
|
||||
contentContainerStyle={[staticStyles.contentContainer, contentContainerStyle]}
|
||||
{...rest}
|
||||
|
||||
@ -14,8 +14,6 @@ export const BlueDefaultTheme = {
|
||||
foregroundColor: '#0c2550',
|
||||
borderTopColor: 'rgba(0, 0, 0, 0.1)',
|
||||
buttonBackgroundColor: '#ccddf9',
|
||||
/** Softer fill for native iOS 26+ prominent header bar buttons (derived from `buttonBackgroundColor`). */
|
||||
headerProminentButtonBackgroundColor: 'rgba(204, 221, 249, 0.9)',
|
||||
buttonTextColor: '#0c2550',
|
||||
secondButtonTextColor: '#50555C',
|
||||
buttonAlternativeTextColor: '#2f5fb3',
|
||||
@ -103,7 +101,6 @@ export const BlueDarkTheme: Theme = {
|
||||
foregroundColor: '#ffffff',
|
||||
buttonDisabledBackgroundColor: '#3A3A3C',
|
||||
buttonBackgroundColor: '#3A3A3C',
|
||||
headerProminentButtonBackgroundColor: 'rgba(58, 58, 60, 0.6)',
|
||||
buttonTextColor: '#ffffff',
|
||||
lightButton: 'rgba(255,255,255,.1)',
|
||||
buttonAlternativeTextColor: '#ffffff',
|
||||
|
||||
@ -245,6 +245,8 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>UIDesignRequiresCompatibility</key>
|
||||
<true/>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Animated, AppState, View, Platform, PlatformColor, Text, StyleSheet, Pressable } from 'react-native';
|
||||
import type { NativeStackHeaderItem, NativeStackNavigationOptions } from '@react-navigation/native-stack';
|
||||
import { NativeStackNavigationOptions } from '@react-navigation/native-stack';
|
||||
import navigationStyle, { CloseButtonPosition } from '../components/navigationStyle';
|
||||
import { useTheme } from '../components/themes';
|
||||
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
|
||||
@ -57,9 +57,6 @@ import { ConnectionPollContext } from './ConnectionPollContext';
|
||||
import ManageWallets from '../screen/wallets/ManageWallets';
|
||||
import ReceiveDetails from '../screen/receive/ReceiveDetails';
|
||||
import ReceiveCustomAmountSheet from '../screen/receive/ReceiveCustomAmountSheet';
|
||||
import { isIOS26OrHigher } from '../components/platform';
|
||||
|
||||
type HeaderRightItem = ReturnType<NonNullable<NativeStackNavigationOptions['unstable_headerRightItems']>>[number];
|
||||
|
||||
const PaymentCodesList = lazy(() => import('../screen/wallets/PaymentCodesList'));
|
||||
const PaymentCodesListComponent = withLazySuspense(PaymentCodesList);
|
||||
@ -153,15 +150,6 @@ const DetailViewStackScreensStack = () => {
|
||||
navigation.navigate('AddWalletRoot');
|
||||
}, [navigation]);
|
||||
|
||||
const navigateToSettings = useCallback(() => {
|
||||
navigation.navigate('DrawerRoot', {
|
||||
screen: 'DetailViewStackScreensStack',
|
||||
params: {
|
||||
screen: 'Settings',
|
||||
},
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
const RightBarButtons = useMemo(
|
||||
() =>
|
||||
sizeClass === SizeClass.Large ? (
|
||||
@ -231,53 +219,6 @@ const DetailViewStackScreensStack = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
if (isIOS26OrHigher) {
|
||||
// Status pills: `unstable_headerLeftItems` + `hidesSharedBackground` avoids the
|
||||
// navigation bar's shared liquid-glass chrome on the pill (solid colors only).
|
||||
return {
|
||||
title: sizeClass === SizeClass.Large ? loc.wallets.list_title : '',
|
||||
headerLargeTitle: false,
|
||||
headerTransparent: true,
|
||||
unstable_headerLeftItems: (): NativeStackHeaderItem[] => {
|
||||
const element = renderHeaderLeft();
|
||||
if (element == null) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: 'custom', element, hidesSharedBackground: true }];
|
||||
},
|
||||
unstable_headerRightItems: () => {
|
||||
if (isDesktop) {
|
||||
return [];
|
||||
}
|
||||
const items: HeaderRightItem[] = [
|
||||
{
|
||||
type: 'button',
|
||||
label: loc.wallets.add_title,
|
||||
icon: { type: 'sfSymbol', name: 'plus' },
|
||||
variant: 'prominent',
|
||||
tintColor: theme.colors.headerProminentButtonBackgroundColor,
|
||||
identifier: 'AddWalletButton',
|
||||
accessibilityLabel: 'AddWalletButton',
|
||||
sharesBackground: false,
|
||||
onPress: navigateToAddWallet,
|
||||
},
|
||||
];
|
||||
if (sizeClass !== SizeClass.Large) {
|
||||
items.push({
|
||||
type: 'button',
|
||||
label: loc.settings.default_title,
|
||||
icon: { type: 'sfSymbol', name: 'ellipsis' },
|
||||
identifier: 'SettingsButton',
|
||||
accessibilityLabel: 'SettingsButton',
|
||||
sharesBackground: false,
|
||||
onPress: navigateToSettings,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: sizeClass === SizeClass.Large ? loc.wallets.list_title : '',
|
||||
headerLargeTitle: false,
|
||||
@ -292,7 +233,6 @@ const DetailViewStackScreensStack = () => {
|
||||
RightBarButtons,
|
||||
sizeClass,
|
||||
theme.colors.customHeader,
|
||||
theme.colors.headerProminentButtonBackgroundColor,
|
||||
theme.colors.foregroundColor,
|
||||
theme.colors.lightButton,
|
||||
theme.colors.redBG,
|
||||
@ -302,8 +242,6 @@ const DetailViewStackScreensStack = () => {
|
||||
electrumConnected,
|
||||
isElectrumDisabled,
|
||||
navigateToElectrumSettings,
|
||||
navigateToAddWallet,
|
||||
navigateToSettings,
|
||||
walletTransactionUpdateStatus,
|
||||
]);
|
||||
|
||||
@ -313,14 +251,6 @@ const DetailViewStackScreensStack = () => {
|
||||
|
||||
// Consistent header configuration for all settings screens
|
||||
const getSettingsHeaderOptions = (title: string) => {
|
||||
if (isIOS26OrHigher) {
|
||||
return {
|
||||
title,
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleShadowVisible: true,
|
||||
headerBackButtonDisplayMode: 'minimal' as const,
|
||||
};
|
||||
}
|
||||
// Use PlatformColor for iOS to match the Settings component, fallback to theme color
|
||||
const titleColor = Platform.OS === 'ios' ? PlatformColor('label') : theme.colors.foregroundColor;
|
||||
// Convert PlatformColor to string for TypeScript compatibility
|
||||
@ -343,9 +273,6 @@ const DetailViewStackScreensStack = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const settingsScreenOptions = (title: string) =>
|
||||
isIOS26OrHigher ? getSettingsHeaderOptions(title) : navigationStyle(getSettingsHeaderOptions(title))(theme);
|
||||
|
||||
return (
|
||||
<ConnectionPollContext.Provider value={connectionPollContextValue}>
|
||||
<DetailViewStack.Navigator
|
||||
@ -412,14 +339,22 @@ const DetailViewStackScreensStack = () => {
|
||||
options={navigationStyle({ title: loc.lndViewInvoice.additional_info })(theme)}
|
||||
/>
|
||||
|
||||
<DetailViewStack.Screen name="Broadcast" component={Broadcast} options={settingsScreenOptions(loc.send.create_broadcast)} />
|
||||
<DetailViewStack.Screen
|
||||
name="Broadcast"
|
||||
component={Broadcast}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.send.create_broadcast))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="IsItMyAddress"
|
||||
component={IsItMyAddress}
|
||||
initialParams={{ address: undefined }}
|
||||
options={settingsScreenOptions(loc.is_it_my_address.title)}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.is_it_my_address.title))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="GenerateWord"
|
||||
component={GenerateWord}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.autofill_word.title))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen name="GenerateWord" component={GenerateWord} options={settingsScreenOptions(loc.autofill_word.title)} />
|
||||
<DetailViewStack.Screen
|
||||
name="LnurlPay"
|
||||
component={LnurlPay}
|
||||
@ -462,90 +397,115 @@ const DetailViewStackScreensStack = () => {
|
||||
<DetailViewStack.Screen
|
||||
name="Settings"
|
||||
component={Settings}
|
||||
options={
|
||||
isIOS26OrHigher
|
||||
? getSettingsHeaderOptions(loc.settings.header)
|
||||
: navigationStyle({
|
||||
title: loc.settings.header,
|
||||
headerBackButtonDisplayMode: 'minimal',
|
||||
headerBackTitle: '',
|
||||
headerShadowVisible: false,
|
||||
// headerLargeTitle is iOS-only, disable on Android for better compatibility with older versions
|
||||
headerLargeTitle: Platform.OS === 'ios',
|
||||
headerLargeTitleStyle:
|
||||
Platform.OS === 'ios'
|
||||
? {
|
||||
color:
|
||||
typeof theme.colors.foregroundColor === 'string'
|
||||
? theme.colors.foregroundColor
|
||||
: String(theme.colors.foregroundColor),
|
||||
}
|
||||
: undefined,
|
||||
headerTitleStyle: {
|
||||
options={navigationStyle({
|
||||
title: loc.settings.header,
|
||||
headerBackButtonDisplayMode: 'minimal',
|
||||
headerBackTitle: '',
|
||||
headerShadowVisible: false,
|
||||
// headerLargeTitle is iOS-only, disable on Android for better compatibility with older versions
|
||||
headerLargeTitle: Platform.OS === 'ios',
|
||||
headerLargeTitleStyle:
|
||||
Platform.OS === 'ios'
|
||||
? {
|
||||
color:
|
||||
typeof theme.colors.foregroundColor === 'string'
|
||||
? theme.colors.foregroundColor
|
||||
: String(theme.colors.foregroundColor),
|
||||
},
|
||||
headerTransparent: false,
|
||||
headerBlurEffect: undefined,
|
||||
headerStyle: {
|
||||
backgroundColor: settingsHeaderBackgroundColor,
|
||||
},
|
||||
animationTypeForReplace: 'push',
|
||||
})(theme)
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
headerTitleStyle: {
|
||||
color: typeof theme.colors.foregroundColor === 'string' ? theme.colors.foregroundColor : String(theme.colors.foregroundColor),
|
||||
},
|
||||
headerTransparent: false,
|
||||
headerBlurEffect: undefined,
|
||||
headerStyle: {
|
||||
backgroundColor: settingsHeaderBackgroundColor,
|
||||
},
|
||||
animationTypeForReplace: 'push',
|
||||
})(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="Currency"
|
||||
component={Currency}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.currency))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="GeneralSettings"
|
||||
component={GeneralSettings}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.general))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen name="Currency" component={Currency} options={settingsScreenOptions(loc.settings.currency)} />
|
||||
<DetailViewStack.Screen name="GeneralSettings" component={GeneralSettings} options={settingsScreenOptions(loc.settings.general)} />
|
||||
<DetailViewStack.Screen
|
||||
name="PlausibleDeniability"
|
||||
component={PlausibleDeniability}
|
||||
options={settingsScreenOptions(loc.plausibledeniability.title)}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.plausibledeniability.title))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="Licensing"
|
||||
component={Licensing}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.license))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="NetworkSettings"
|
||||
component={NetworkSettings}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.network))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen name="Licensing" component={Licensing} options={settingsScreenOptions(loc.settings.license)} />
|
||||
<DetailViewStack.Screen name="NetworkSettings" component={NetworkSettings} options={settingsScreenOptions(loc.settings.network)} />
|
||||
<DetailViewStack.Screen
|
||||
name="SettingsBlockExplorer"
|
||||
component={SettingsBlockExplorer}
|
||||
options={settingsScreenOptions(loc.settings.block_explorer)}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.block_explorer))(theme)}
|
||||
/>
|
||||
|
||||
<DetailViewStack.Screen name="About" component={About} options={settingsScreenOptions(loc.settings.about)} />
|
||||
<DetailViewStack.Screen
|
||||
name="About"
|
||||
component={About}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.about))(theme)}
|
||||
/>
|
||||
{/* <DetailViewStack.Screen
|
||||
name="DefaultView"
|
||||
component={DefaultView}
|
||||
options={settingsScreenOptions(loc.settings.default_title)}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.default_title))(theme)}
|
||||
/> */}
|
||||
<DetailViewStack.Screen
|
||||
name="ElectrumSettings"
|
||||
component={ElectrumSettings}
|
||||
options={settingsScreenOptions(loc.settings.electrum_settings_server)}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.electrum_settings_server))(theme)}
|
||||
initialParams={{ server: undefined }}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="EncryptStorage"
|
||||
component={EncryptStorage}
|
||||
options={settingsScreenOptions(loc.settings.encrypt_title)}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.encrypt_title))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="Language"
|
||||
component={Language}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.language))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen name="Language" component={Language} options={settingsScreenOptions(loc.settings.language)} />
|
||||
<DetailViewStack.Screen
|
||||
name="LightningSettings"
|
||||
component={LightningSettings}
|
||||
options={settingsScreenOptions(loc.settings.lightning_settings)}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.lightning_settings))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="NotificationSettings"
|
||||
component={NotificationSettings}
|
||||
options={settingsScreenOptions(loc.settings.notifications)}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.notifications))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="SelfTest"
|
||||
component={SelfTest}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.selfTest))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen name="SelfTest" component={SelfTest} options={settingsScreenOptions(loc.settings.selfTest)} />
|
||||
<DetailViewStack.Screen
|
||||
name="ReleaseNotes"
|
||||
component={ReleaseNotes}
|
||||
options={settingsScreenOptions(loc.settings.about_release_notes)}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.about_release_notes))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="SettingsTools"
|
||||
component={SettingsTools}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.tools))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen name="SettingsTools" component={SettingsTools} options={settingsScreenOptions(loc.settings.tools)} />
|
||||
<DetailViewStack.Screen
|
||||
name="PromptPasswordConfirmationSheet"
|
||||
component={PromptPasswordConfirmationSheet}
|
||||
|
||||
@ -1,98 +1,44 @@
|
||||
import React from 'react';
|
||||
import { Platform, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import type { NativeStackHeaderItem, NativeStackNavigationOptions } from '@react-navigation/native-stack';
|
||||
import { TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import Icon from '../../components/Icon';
|
||||
import WalletGradient from '../../class/wallet-gradient';
|
||||
import { NativeStackNavigationOptions } from '@react-navigation/native-stack';
|
||||
import { DetailViewStackParamList } from '../DetailViewStackParamList';
|
||||
import { navigationRef } from '../../NavigationService';
|
||||
import { RouteProp } from '@react-navigation/native';
|
||||
import { isDesktop } from '../../blue_modules/environment';
|
||||
import { isIOS26OrHigher } from '../../components/platform';
|
||||
import loc from '../../loc';
|
||||
|
||||
export type WalletTransactionsRouteProps = RouteProp<DetailViewStackParamList, 'WalletTransactions'>;
|
||||
|
||||
const HERO_HEADER_ICON_COLOR = '#FFFFFF';
|
||||
const getWalletTransactionsOptions = ({ route }: { route: WalletTransactionsRouteProps }): NativeStackNavigationOptions => {
|
||||
const { isLoading = false, walletID, walletType } = route.params;
|
||||
|
||||
const navigateToWalletDetails = (walletID: string) => {
|
||||
navigationRef.navigate('WalletDetails', {
|
||||
walletID,
|
||||
});
|
||||
};
|
||||
const onPress = () => {
|
||||
navigationRef.navigate('WalletDetails', {
|
||||
walletID,
|
||||
});
|
||||
};
|
||||
|
||||
/** Material "more" button for WalletTransactions header (pre–iOS 26 and Android). */
|
||||
export const createWalletDetailsHeaderRight = ({
|
||||
walletID,
|
||||
isLoading = false,
|
||||
iconColor = HERO_HEADER_ICON_COLOR,
|
||||
}: {
|
||||
walletID: string;
|
||||
isLoading?: boolean;
|
||||
iconColor?: string;
|
||||
}): (() => React.ReactElement) => {
|
||||
return () => (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
testID="WalletDetails"
|
||||
disabled={isLoading}
|
||||
style={styles.walletDetails}
|
||||
onPress={() => navigateToWalletDetails(walletID)}
|
||||
>
|
||||
<Icon name="more-horiz" type="material" size={22} color={iconColor} />
|
||||
const RightButton = (
|
||||
<TouchableOpacity accessibilityRole="button" testID="WalletDetails" disabled={isLoading} style={styles.walletDetails} onPress={onPress}>
|
||||
<Icon name="more-horiz" type="material" size={22} color="#FFFFFF" />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
/** Native toolbar ellipsis for WalletTransactions on iOS 26+. */
|
||||
export const createWalletDetailsHeaderRightItems = ({
|
||||
isLoading = false,
|
||||
walletID,
|
||||
}: {
|
||||
isLoading?: boolean;
|
||||
walletID: string;
|
||||
}): (() => NativeStackHeaderItem[]) => {
|
||||
return () => [
|
||||
{
|
||||
type: 'button',
|
||||
label: loc.wallets.details_title,
|
||||
icon: { type: 'sfSymbol', name: 'ellipsis' },
|
||||
identifier: 'WalletDetails',
|
||||
accessibilityLabel: 'WalletDetails',
|
||||
sharesBackground: false,
|
||||
onPress: () => navigateToWalletDetails(walletID),
|
||||
disabled: isLoading,
|
||||
},
|
||||
];
|
||||
};
|
||||
const backgroundColor = WalletGradient.headerColorFor(walletType);
|
||||
|
||||
const getWalletTransactionsOptions = ({ route }: { route: WalletTransactionsRouteProps }): NativeStackNavigationOptions => {
|
||||
const { isLoading = false, walletID } = route.params;
|
||||
|
||||
const base: NativeStackNavigationOptions = {
|
||||
return {
|
||||
title: '',
|
||||
headerBackTitleStyle: { fontSize: 0 },
|
||||
headerTransparent: true,
|
||||
headerStyle: {
|
||||
backgroundColor: 'transparent',
|
||||
backgroundColor,
|
||||
},
|
||||
headerBackButtonDisplayMode: 'minimal',
|
||||
headerShadowVisible: false,
|
||||
headerTintColor: HERO_HEADER_ICON_COLOR,
|
||||
headerBlurEffect: undefined,
|
||||
headerTintColor: '#FFFFFF',
|
||||
statusBarStyle: 'light',
|
||||
headerBackTitle: undefined,
|
||||
headerRight: createWalletDetailsHeaderRight({ walletID, isLoading, iconColor: HERO_HEADER_ICON_COLOR }),
|
||||
headerRight: () => RightButton,
|
||||
};
|
||||
|
||||
if (Platform.OS === 'ios' && isIOS26OrHigher && !isDesktop) {
|
||||
return {
|
||||
...base,
|
||||
headerRight: undefined,
|
||||
experimental_userInterfaceStyle: 'dark' as const,
|
||||
unstable_headerRightItems: createWalletDetailsHeaderRightItems({ isLoading, walletID }),
|
||||
};
|
||||
}
|
||||
|
||||
return base;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
148
package-lock.json
generated
148
package-lock.json
generated
@ -17,8 +17,7 @@
|
||||
"@bugsnag/source-maps": "2.3.3",
|
||||
"@keystonehq/bc-ur-registry": "0.7.1",
|
||||
"@ngraveio/bc-ur": "1.1.13",
|
||||
"@noble/ciphers": "1.3.0",
|
||||
"@noble/hashes": "1.8.0",
|
||||
"@noble/hashes": "1.3.3",
|
||||
"@noble/secp256k1": "3.1.0",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-clipboard/clipboard": "1.16.3",
|
||||
@ -36,10 +35,10 @@
|
||||
"@react-native/codegen": "0.85.3",
|
||||
"@react-native/gradle-plugin": "0.85.3",
|
||||
"@react-native/metro-config": "0.85.3",
|
||||
"@react-navigation/devtools": "7.0.62",
|
||||
"@react-navigation/drawer": "7.12.0",
|
||||
"@react-navigation/native": "7.3.1",
|
||||
"@react-navigation/native-stack": "7.17.3",
|
||||
"@react-navigation/devtools": "7.0.58",
|
||||
"@react-navigation/drawer": "7.10.2",
|
||||
"@react-navigation/native": "7.2.4",
|
||||
"@react-navigation/native-stack": "7.15.1",
|
||||
"@scure/base": "2.0.0",
|
||||
"@spsina/bip47": "github:BlueWallet/bip47#df82345",
|
||||
"aezeed": "0.0.5",
|
||||
@ -58,6 +57,7 @@
|
||||
"buffer": "6.0.3",
|
||||
"coinselect": "github:BlueWallet/coinselect#35f8038",
|
||||
"crypto-browserify": "3.12.1",
|
||||
"crypto-js": "4.2.0",
|
||||
"dayjs": "1.11.21",
|
||||
"detox": "20.51.3",
|
||||
"ecpair": "3.0.1",
|
||||
@ -119,13 +119,14 @@
|
||||
"@jest/reporters": "^27.5.1",
|
||||
"@react-native/eslint-config": "^0.85.3",
|
||||
"@react-native/jest-preset": "0.85.3",
|
||||
"@react-native/js-polyfills": "^0.86.0",
|
||||
"@react-native/js-polyfills": "^0.85.3",
|
||||
"@react-native/metro-babel-transformer": "^0.85.3",
|
||||
"@react-native/typescript-config": "^0.85.3",
|
||||
"@testing-library/react-native": "^13.0.1",
|
||||
"@types/bip38": "^3.1.2",
|
||||
"@types/bs58check": "^2.1.0",
|
||||
"@types/create-hash": "^1.2.2",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-test-renderer": "^19.1.0",
|
||||
@ -3496,18 +3497,6 @@
|
||||
"eslint-scope": "5.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
|
||||
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz",
|
||||
@ -3536,12 +3525,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"version": "1.3.3",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
@ -4714,16 +4701,6 @@
|
||||
"react": "^19.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/jest-preset/node_modules/@react-native/js-polyfills": {
|
||||
"version": "0.85.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.85.3.tgz",
|
||||
"integrity": "sha512-U2+aMshIXf1uFn77tpBb/xhHWB9vkVrMpt7kkucAugF8hJKYTDGB587X7WwelHduK2KBfhl4giSv0rzZGoef9A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/jest-preset/node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
@ -4732,10 +4709,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-native/js-polyfills": {
|
||||
"version": "0.86.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.86.0.tgz",
|
||||
"integrity": "sha512-zYy/Cjd1VTnZ2iCNaG9bDF9C3l2ntESiPRscjIlI5FKugu6aeTwsDSv1aI8Bc4Kp3vEdoVg+UQhLAhE4svREaQ==",
|
||||
"dev": true,
|
||||
"version": "0.85.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.85.3.tgz",
|
||||
"integrity": "sha512-U2+aMshIXf1uFn77tpBb/xhHWB9vkVrMpt7kkucAugF8hJKYTDGB587X7WwelHduK2KBfhl4giSv0rzZGoef9A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
|
||||
@ -4774,15 +4750,6 @@
|
||||
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/metro-config/node_modules/@react-native/js-polyfills": {
|
||||
"version": "0.85.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.85.3.tgz",
|
||||
"integrity": "sha512-U2+aMshIXf1uFn77tpBb/xhHWB9vkVrMpt7kkucAugF8hJKYTDGB587X7WwelHduK2KBfhl4giSv0rzZGoef9A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/normalize-colors": {
|
||||
"version": "0.85.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.85.3.tgz",
|
||||
@ -4820,12 +4787,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/core": {
|
||||
"version": "7.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.20.0.tgz",
|
||||
"integrity": "sha512-Lqw5cDQWWxiQnaWv6RhQV95Wr4fh+38/IFVNn1grssyLWV+wXGJjlucXOoU7EVh9jdtcLT8pGyzvsyrvSDywWA==",
|
||||
"version": "7.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.17.4.tgz",
|
||||
"integrity": "sha512-Rv9E2oNNQEkPGpmu9q+vJwGJRSQR6LBg5L+Yo1QHjtwGbHUbjkIKOdYymDZoZYgNzX2OD4rAIlfuzbDKa3cCeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^7.6.0",
|
||||
"@react-navigation/routers": "^7.5.5",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"nanoid": "^3.3.11",
|
||||
@ -4845,9 +4812,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-navigation/devtools": {
|
||||
"version": "7.0.62",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/devtools/-/devtools-7.0.62.tgz",
|
||||
"integrity": "sha512-Xl+HhZmz0tzJCH13KCs19xYQWPfkQFfYd7Mxv5MnpFdYuxkmvedPJilwAhcTtJc+4PMtQ7sR0Jqv7Ssg4CPblg==",
|
||||
"version": "7.0.58",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/devtools/-/devtools-7.0.58.tgz",
|
||||
"integrity": "sha512-WpADcM0n+QHP1RMMmKZPc4reuvwTyX41gnJCdipjNUG0+VBNOkDyJZpAkeJqOJg2BIjSwsKcTAph3xkmXBjXVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@ -4859,18 +4826,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/drawer": {
|
||||
"version": "7.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/drawer/-/drawer-7.12.0.tgz",
|
||||
"integrity": "sha512-OP8ti/ESCPng79/UzafQxYYP/EVHmgSCnNL91RGnT3ghsIpjr8xut5Ax+5N5+vwfEWBbHaxPCeuVHwukcmdtQw==",
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/drawer/-/drawer-7.10.2.tgz",
|
||||
"integrity": "sha512-/ccYFvBPJNzOYioiMQsqjAR4dcQ+7+yjzcuMDTKgsMahLD7Jn7FdOFNtGwMaIQWhfK8KFVMH2KOXAlH/uAGZXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-navigation/elements": "^2.9.23",
|
||||
"@react-navigation/elements": "^2.9.18",
|
||||
"color": "^4.2.3",
|
||||
"react-native-drawer-layout": "^4.2.5",
|
||||
"react-native-drawer-layout": "^4.2.4",
|
||||
"use-latest-callback": "^0.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-navigation/native": "^7.3.1",
|
||||
"@react-navigation/native": "^7.2.4",
|
||||
"react": ">= 18.2.0",
|
||||
"react-native": "*",
|
||||
"react-native-gesture-handler": ">= 2.0.0",
|
||||
@ -4880,9 +4847,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/elements": {
|
||||
"version": "2.9.23",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.23.tgz",
|
||||
"integrity": "sha512-sp+FgihDyMBoEXoCUsUCT/iibN/sg6LYGq/rciy6NjT8bnfv4Cu3el8SAaJ0bfRG3tdchHy6gweKmcaJs/BAYQ==",
|
||||
"version": "2.9.18",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.18.tgz",
|
||||
"integrity": "sha512-mKEvDr6CkCVYZSb8W9WubNseihL+1c8M7ktZJCTCbMk8rQgdQfkdRNwpSUQKspdGpUHCb9cyzvaiuzl1NtjVgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
@ -4891,7 +4858,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-native-masked-view/masked-view": ">= 0.2.0",
|
||||
"@react-navigation/native": "^7.3.1",
|
||||
"@react-navigation/native": "^7.2.4",
|
||||
"react": ">= 18.2.0",
|
||||
"react-native": "*",
|
||||
"react-native-safe-area-context": ">= 4.0.0"
|
||||
@ -4903,16 +4870,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/native": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.3.1.tgz",
|
||||
"integrity": "sha512-g1o8jBm87WviR0Eq0wT0M43TSi+uBTz4x8YfHh4XRQ+FHqhNr+uGbuxtGu72QhHtOz0LWnb8UWyvd+M6xWkWHQ==",
|
||||
"version": "7.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.4.tgz",
|
||||
"integrity": "sha512-eWC2D3JjhYLId2fVTZhhCiUpWIaPhO9XyEb7Wq8ElmOHyIODlbOzgZ0rKia02OIsDKr9BzZl2sK1dL70yMxDaw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^7.20.0",
|
||||
"@react-navigation/core": "^7.17.4",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"nanoid": "^3.3.11",
|
||||
"standard-navigation": "^0.0.7",
|
||||
"use-latest-callback": "^0.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@ -4921,18 +4887,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/native-stack": {
|
||||
"version": "7.17.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.17.3.tgz",
|
||||
"integrity": "sha512-8X9AxW0BACB62eCL+DAL+Nf5lFAxXi3w1qaj2D/i0axYjxUZbI5AwrfuHjRo0B231K5WWa6HKyscF07IDHcKHg==",
|
||||
"version": "7.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.15.1.tgz",
|
||||
"integrity": "sha512-kNrJggwoB/onC0MpZIuZ6qaqeAziFchz+W9txBzhd6qbWmB1OkPVUnu6fWgc6BQc7MeMf59djVmqgX+6kJU1Ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-navigation/elements": "^2.9.23",
|
||||
"@react-navigation/elements": "^2.9.18",
|
||||
"color": "^4.2.3",
|
||||
"sf-symbols-typescript": "^2.1.0",
|
||||
"warn-once": "^0.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-navigation/native": "^7.3.1",
|
||||
"@react-navigation/native": "^7.2.4",
|
||||
"react": ">= 18.2.0",
|
||||
"react-native": "*",
|
||||
"react-native-safe-area-context": ">= 4.0.0",
|
||||
@ -4940,9 +4906,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/routers": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.6.0.tgz",
|
||||
"integrity": "sha512-lblhDXfS75jLc7G2K7BZGM+7cjqQXk13X/MA4fq/12r62zM+fBhhreLzYflSitrDDXFRJpSvJXy0ziiGU04Xow==",
|
||||
"version": "7.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.5.tgz",
|
||||
"integrity": "sha512-9/hhMte12Kgu+pMnLfA4EWJ0OQmIEAMVMX06FPH2yGkEQSQ3JhhCN/GkcRikzQhtEi97VYYQA15umptBUShcOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11"
|
||||
@ -5276,6 +5242,11 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/crypto-js": {
|
||||
"version": "4.2.2",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.9",
|
||||
"dev": true,
|
||||
@ -7963,6 +7934,10 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.1.0",
|
||||
"license": "BSD-2-Clause",
|
||||
@ -16284,9 +16259,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-drawer-layout": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-4.2.5.tgz",
|
||||
"integrity": "sha512-Yl82uLkXjXuq7222hWGIDsq5A6R/bsCeCEgdIxQUxAEHf00oRdDnRByLx3Fsij3qwtmYNPGrHV1NH8G8hbCbLQ==",
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-4.2.4.tgz",
|
||||
"integrity": "sha512-l1Le5HcVidobnJm8xqFZo46Rs8FDHdxbTZhkjxpNSRgU+QMoQXilOfzTHAeNjEGiKVGgIs9cW3ctXeHqgp5jJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
@ -16631,15 +16606,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/@react-native/js-polyfills": {
|
||||
"version": "0.85.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.85.3.tgz",
|
||||
"integrity": "sha512-U2+aMshIXf1uFn77tpBb/xhHWB9vkVrMpt7kkucAugF8hJKYTDGB587X7WwelHduK2KBfhl4giSv0rzZGoef9A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"license": "MIT",
|
||||
@ -17890,12 +17856,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/standard-navigation": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/standard-navigation/-/standard-navigation-0.0.7.tgz",
|
||||
"integrity": "sha512-NCGLCNyuXrFOkGHxdNZFnpsehGtiq1oXbPhKl7ZuxFO5J//H2evqqOchmD4YwEUJnkjO4kH9Xp4hQX6hdAYCKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
|
||||
15
package.json
15
package.json
@ -20,13 +20,14 @@
|
||||
"@jest/reporters": "^27.5.1",
|
||||
"@react-native/eslint-config": "^0.85.3",
|
||||
"@react-native/jest-preset": "0.85.3",
|
||||
"@react-native/js-polyfills": "^0.86.0",
|
||||
"@react-native/js-polyfills": "^0.85.3",
|
||||
"@react-native/metro-babel-transformer": "^0.85.3",
|
||||
"@react-native/typescript-config": "^0.85.3",
|
||||
"@testing-library/react-native": "^13.0.1",
|
||||
"@types/bip38": "^3.1.2",
|
||||
"@types/bs58check": "^2.1.0",
|
||||
"@types/create-hash": "^1.2.2",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-test-renderer": "^19.1.0",
|
||||
@ -99,8 +100,7 @@
|
||||
"@bugsnag/source-maps": "2.3.3",
|
||||
"@keystonehq/bc-ur-registry": "0.7.1",
|
||||
"@ngraveio/bc-ur": "1.1.13",
|
||||
"@noble/ciphers": "1.3.0",
|
||||
"@noble/hashes": "1.8.0",
|
||||
"@noble/hashes": "1.3.3",
|
||||
"@noble/secp256k1": "3.1.0",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-clipboard/clipboard": "1.16.3",
|
||||
@ -118,10 +118,10 @@
|
||||
"@react-native/codegen": "0.85.3",
|
||||
"@react-native/gradle-plugin": "0.85.3",
|
||||
"@react-native/metro-config": "0.85.3",
|
||||
"@react-navigation/devtools": "7.0.62",
|
||||
"@react-navigation/drawer": "7.12.0",
|
||||
"@react-navigation/native": "7.3.1",
|
||||
"@react-navigation/native-stack": "7.17.3",
|
||||
"@react-navigation/devtools": "7.0.58",
|
||||
"@react-navigation/drawer": "7.10.2",
|
||||
"@react-navigation/native": "7.2.4",
|
||||
"@react-navigation/native-stack": "7.15.1",
|
||||
"@scure/base": "2.0.0",
|
||||
"@spsina/bip47": "github:BlueWallet/bip47#df82345",
|
||||
"aezeed": "0.0.5",
|
||||
@ -140,6 +140,7 @@
|
||||
"buffer": "6.0.3",
|
||||
"coinselect": "github:BlueWallet/coinselect#35f8038",
|
||||
"crypto-browserify": "3.12.1",
|
||||
"crypto-js": "4.2.0",
|
||||
"dayjs": "1.11.21",
|
||||
"detox": "20.51.3",
|
||||
"ecpair": "3.0.1",
|
||||
|
||||
@ -1,84 +0,0 @@
|
||||
diff --git a/node_modules/@react-navigation/native-stack/lib/module/views/useHeaderConfigProps.js b/node_modules/@react-navigation/native-stack/lib/module/views/useHeaderConfigProps.js
|
||||
index a42477a..3ff714c 100644
|
||||
--- a/node_modules/@react-navigation/native-stack/lib/module/views/useHeaderConfigProps.js
|
||||
+++ b/node_modules/@react-navigation/native-stack/lib/module/views/useHeaderConfigProps.js
|
||||
@@ -159,7 +159,8 @@ export function useHeaderConfigProps({
|
||||
route,
|
||||
title,
|
||||
unstable_headerLeftItems: headerLeftItems,
|
||||
- unstable_headerRightItems: headerRightItems
|
||||
+ unstable_headerRightItems: headerRightItems,
|
||||
+ experimental_userInterfaceStyle: experimentalUserInterfaceStyleOption
|
||||
}) {
|
||||
const {
|
||||
direction
|
||||
@@ -365,7 +366,7 @@ export function useHeaderConfigProps({
|
||||
children,
|
||||
headerLeftBarButtonItems: processBarButtonItems(leftItems, colors, fonts),
|
||||
headerRightBarButtonItems: processBarButtonItems(rightItems, colors, fonts),
|
||||
- experimental_userInterfaceStyle: dark ? 'dark' : 'light'
|
||||
+ experimental_userInterfaceStyle: experimentalUserInterfaceStyleOption ?? (dark ? 'dark' : 'light')
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=useHeaderConfigProps.js.map
|
||||
\ No newline at end of file
|
||||
diff --git a/node_modules/@react-navigation/native-stack/lib/typescript/src/types.d.ts b/node_modules/@react-navigation/native-stack/lib/typescript/src/types.d.ts
|
||||
index 2f1351a..5742b66 100644
|
||||
--- a/node_modules/@react-navigation/native-stack/lib/typescript/src/types.d.ts
|
||||
+++ b/node_modules/@react-navigation/native-stack/lib/typescript/src/types.d.ts
|
||||
@@ -302,6 +302,14 @@ export type NativeStackNavigationOptions = {
|
||||
* @platform ios
|
||||
*/
|
||||
unstable_headerRightItems?: (props: NativeStackHeaderItemProps) => NativeStackHeaderItem[];
|
||||
+ /**
|
||||
+ * When set, overrides the navigation header `UIUserInterfaceStyle` (affects iOS 26+ bar materials and tint resolution).
|
||||
+ * If omitted, React Navigation sets this from the navigation theme `dark` boolean (`true` → `"dark"`, else `"light"`).
|
||||
+ *
|
||||
+ * @platform ios
|
||||
+ * @experimental
|
||||
+ */
|
||||
+ experimental_userInterfaceStyle?: import('react-native-screens').ScreenStackHeaderConfigProps['experimental_userInterfaceStyle'];
|
||||
/**
|
||||
* String or a function that returns a React Element to be used by the header.
|
||||
* Defaults to screen `title` or route name.
|
||||
diff --git a/node_modules/@react-navigation/native-stack/src/types.tsx b/node_modules/@react-navigation/native-stack/src/types.tsx
|
||||
index 7488b1c..542333e 100644
|
||||
--- a/node_modules/@react-navigation/native-stack/src/types.tsx
|
||||
+++ b/node_modules/@react-navigation/native-stack/src/types.tsx
|
||||
@@ -350,6 +350,15 @@ export type NativeStackNavigationOptions = {
|
||||
unstable_headerRightItems?: (
|
||||
props: NativeStackHeaderItemProps
|
||||
) => NativeStackHeaderItem[];
|
||||
+ /**
|
||||
+ * When set, overrides the navigation header `UIUserInterfaceStyle` (affects iOS 26+ bar materials and tint resolution).
|
||||
+ * If omitted, React Navigation sets this from the navigation theme `dark` boolean (`true` → `"dark"`, else `"light"`).
|
||||
+ *
|
||||
+ * @platform ios
|
||||
+ * @experimental
|
||||
+ * @see {@link https://github.com/react-navigation/react-navigation/issues/13069}
|
||||
+ */
|
||||
+ experimental_userInterfaceStyle?: ScreenStackHeaderConfigProps['experimental_userInterfaceStyle'];
|
||||
/**
|
||||
* String or a function that returns a React Element to be used by the header.
|
||||
* Defaults to screen `title` or route name.
|
||||
diff --git a/node_modules/@react-navigation/native-stack/src/views/useHeaderConfigProps.tsx b/node_modules/@react-navigation/native-stack/src/views/useHeaderConfigProps.tsx
|
||||
index 6f74856..d12cf7d 100644
|
||||
--- a/node_modules/@react-navigation/native-stack/src/views/useHeaderConfigProps.tsx
|
||||
+++ b/node_modules/@react-navigation/native-stack/src/views/useHeaderConfigProps.tsx
|
||||
@@ -217,6 +217,7 @@ export function useHeaderConfigProps({
|
||||
title,
|
||||
unstable_headerLeftItems: headerLeftItems,
|
||||
unstable_headerRightItems: headerRightItems,
|
||||
+ experimental_userInterfaceStyle: experimentalUserInterfaceStyleOption,
|
||||
}: Props): ScreenStackHeaderConfigProps {
|
||||
const { direction } = useLocale();
|
||||
const { colors, fonts, dark } = useTheme();
|
||||
@@ -527,6 +528,7 @@ export function useHeaderConfigProps({
|
||||
children,
|
||||
headerLeftBarButtonItems: processBarButtonItems(leftItems, colors, fonts),
|
||||
headerRightBarButtonItems: processBarButtonItems(rightItems, colors, fonts),
|
||||
- experimental_userInterfaceStyle: dark ? 'dark' : 'light',
|
||||
+ experimental_userInterfaceStyle:
|
||||
+ experimentalUserInterfaceStyleOption ?? (dark ? 'dark' : 'light'),
|
||||
} as const;
|
||||
}
|
||||
@ -64,48 +64,3 @@ delivered.
|
||||
Added in BlueWallet PR https://github.com/BlueWallet/BlueWallet/pull/8424
|
||||
during a React Native bump. Remove once `react-native-notifications`
|
||||
ships New-Architecture-safe token delivery.
|
||||
|
||||
---
|
||||
|
||||
## `@react-navigation+native-stack+7.15.1.patch`
|
||||
|
||||
**What:** adds an `experimental_userInterfaceStyle` navigation option to
|
||||
`NativeStackNavigationOptions` (typed in `src/types.tsx` and the built
|
||||
`lib/typescript` d.ts) and threads it through `useHeaderConfigProps` so a
|
||||
screen can override the header's `UIUserInterfaceStyle`. When omitted it
|
||||
falls back to the previous behaviour via
|
||||
`experimentalUserInterfaceStyleOption ?? (dark ? 'dark' : 'light')`.
|
||||
|
||||
**Why:** on iOS 26 the navigation bar's liquid-glass material and tint are
|
||||
resolved from `UIUserInterfaceStyle`. React Navigation hard-codes this from
|
||||
the theme `dark` boolean, so a screen cannot force a light/dark header
|
||||
independent of the active theme. The iOS 26 glass header
|
||||
(`screen/wallets/WalletTransactions.tsx`) needs that per-screen override.
|
||||
|
||||
**Upstream:** https://github.com/react-navigation/react-navigation/issues/13069 (open)
|
||||
|
||||
Added in BlueWallet PR https://github.com/BlueWallet/BlueWallet/pull/8508.
|
||||
Remove once `@react-navigation/native-stack` exposes a header
|
||||
`UIUserInterfaceStyle` override upstream. When bumping the dependency,
|
||||
rename this patch to the new version and re-confirm the hunks still apply
|
||||
(`npx patch-package`).
|
||||
|
||||
---
|
||||
|
||||
## `react-native-screens+4.25.2.patch`
|
||||
|
||||
**What:** in `RNSBarButtonItem.mm`, also set `self.accessibilityIdentifier`
|
||||
when the JS `identifier` is provided (one line, alongside the existing
|
||||
`self.identifier = identifier`).
|
||||
|
||||
**Why:** the iOS 26 glass header builds nav-bar buttons through
|
||||
`unstable_headerRightItems`. The native `identifier` is not exposed as an
|
||||
accessibility identifier, so Detox/XCUITest could not target those bar
|
||||
buttons. Mirroring it onto `accessibilityIdentifier` makes them reachable
|
||||
from e2e tests.
|
||||
|
||||
**Upstream:** no issue filed yet — local accessibility enhancement.
|
||||
|
||||
Added in BlueWallet PR https://github.com/BlueWallet/BlueWallet/pull/8508.
|
||||
When bumping `react-native-screens`, rename this patch to the new version
|
||||
and re-confirm the hunk still applies (`npx patch-package`).
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
diff --git a/node_modules/react-native-screens/ios/RNSBarButtonItem.mm b/node_modules/react-native-screens/ios/RNSBarButtonItem.mm
|
||||
index 0eb1f09..324b888 100644
|
||||
--- a/node_modules/react-native-screens/ios/RNSBarButtonItem.mm
|
||||
+++ b/node_modules/react-native-screens/ios/RNSBarButtonItem.mm
|
||||
@@ -81,6 +81,7 @@ - (instancetype)initWithConfig:(NSDictionary<NSString *, id> *)dict
|
||||
NSString *identifier = dict[@"identifier"];
|
||||
if (identifier != nil) {
|
||||
self.identifier = identifier;
|
||||
+ self.accessibilityIdentifier = identifier;
|
||||
}
|
||||
NSDictionary *badgeConfig = dict[@"badge"];
|
||||
if (badgeConfig != nil) {
|
||||
@ -1,29 +1,8 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:best-practices",
|
||||
":disableMajorUpdates",
|
||||
":preserveSemverRanges"
|
||||
],
|
||||
"ignoreDeps": ["react-native"],
|
||||
"schedule": ["before 6am on monday"],
|
||||
"prConcurrentLimit": 1,
|
||||
"prHourlyLimit": 0,
|
||||
"minimumReleaseAge": "3 days",
|
||||
"semanticCommits": "disabled",
|
||||
"commitMessagePrefix": "OPS:",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": ["*"],
|
||||
"groupName": "all dependencies",
|
||||
"groupSlug": "all"
|
||||
}
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": ["before 6am on monday"]
|
||||
},
|
||||
"vulnerabilityAlerts": {
|
||||
"schedule": ["at any time"]
|
||||
}
|
||||
"ignoreDeps": ["react-native"]
|
||||
}
|
||||
|
||||
@ -2,13 +2,7 @@ import React, { useMemo, useLayoutEffect, useCallback } from 'react';
|
||||
import { View, StyleSheet, Linking, Image, Platform } from 'react-native';
|
||||
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
|
||||
import loc from '../../loc';
|
||||
import {
|
||||
SettingsScrollView,
|
||||
SettingsSection,
|
||||
SettingsListItem,
|
||||
getSettingsHeaderOptions,
|
||||
isIOS26OrHigher,
|
||||
} from '../../components/platform';
|
||||
import { SettingsScrollView, SettingsSection, SettingsListItem, getSettingsHeaderOptions } from '../../components/platform';
|
||||
import { useSettings } from '../../hooks/context/useSettings';
|
||||
import { useTheme } from '../../components/themes';
|
||||
|
||||
@ -21,9 +15,6 @@ const Settings = () => {
|
||||
const settingsScreenBackgroundColor = isIOSLightMode ? settingsCardColor : colors.background;
|
||||
const settingsListItemBackgroundColor = isIOSLightMode ? colors.background : undefined;
|
||||
useLayoutEffect(() => {
|
||||
if (isIOS26OrHigher) {
|
||||
return;
|
||||
}
|
||||
setOptions(getSettingsHeaderOptions(loc.settings.header, { ...colors, background: settingsScreenBackgroundColor }, dark));
|
||||
}, [setOptions, language, colors, settingsScreenBackgroundColor, dark]); // Include language to trigger re-render when language changes
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -11,15 +11,9 @@ import {
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
RefreshControl,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import Icon from '../../components/Icon';
|
||||
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
|
||||
import { isDesktop } from '../../blue_modules/environment';
|
||||
@ -33,7 +27,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';
|
||||
@ -42,14 +35,10 @@ import { Chain } from '../../models/bitcoinUnits';
|
||||
import ActionSheet from '../ActionSheet';
|
||||
import { useStorage } from '../../hooks/context/useStorage';
|
||||
import WatchOnlyWarning from '../../components/WatchOnlyWarning';
|
||||
import { NativeStackNavigationOptions, NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
|
||||
import { Transaction, TWallet } from '../../class/wallets/types';
|
||||
import getWalletTransactionsOptions, {
|
||||
WalletTransactionsRouteProps,
|
||||
createWalletDetailsHeaderRight,
|
||||
createWalletDetailsHeaderRightItems,
|
||||
} from '../../navigation/helpers/getWalletTransactionsOptions';
|
||||
import getWalletTransactionsOptions, { WalletTransactionsRouteProps } from '../../navigation/helpers/getWalletTransactionsOptions';
|
||||
import { presentWalletExportReminder } from '../../helpers/presentWalletExportReminder';
|
||||
import selectWallet from '../../helpers/select-wallet';
|
||||
import assert from 'assert';
|
||||
@ -60,8 +49,6 @@ import { getClipboardContent } from '../../blue_modules/clipboard';
|
||||
import HandOffComponent from '../../components/HandOffComponent';
|
||||
import { HandOffActivityType } from '../../components/types';
|
||||
import WalletGradient from '../../class/wallet-gradient';
|
||||
import { isIOS26OrHigher } from '../../components/platform';
|
||||
import Animated, { SharedValue, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
||||
|
||||
const buttonFontSize =
|
||||
PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22
|
||||
@ -72,109 +59,7 @@ type RouteProps = RouteProp<DetailViewStackParamList, 'WalletTransactions'>;
|
||||
|
||||
type WalletTransactionsProps = NativeStackScreenProps<DetailViewStackParamList, 'WalletTransactions'>;
|
||||
|
||||
/** Scroll offset after which the compact wallet name + balance header is shown. */
|
||||
const SCROLLED_HEADER_SHOW_OFFSET = 180;
|
||||
const SCROLLED_HEADER_FADE_IN_MS = 180;
|
||||
const SCROLLED_HEADER_FADE_OUT_MS = 150;
|
||||
|
||||
const usesIos26AnimatedScrolledHeader = Platform.OS === 'ios' && isIOS26OrHigher && !isDesktop;
|
||||
|
||||
/** Native stack options used when scrolled; includes props missing from the published TS types. */
|
||||
type WalletTransactionsScrolledHeaderOptions = NativeStackNavigationOptions & {
|
||||
headerTitleContainerStyle?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
/** Horizontal space reserved so the scrolled title does not run under back / header-right actions. */
|
||||
const getScrolledHeaderTitleLayout = (screenWidth: number) => {
|
||||
const titleInsetLeft = Platform.OS === 'ios' ? (isIOS26OrHigher ? 40 : 56) : 72;
|
||||
const titleInsetRight = Platform.OS === 'ios' ? (isIOS26OrHigher ? 96 : 84) : 84;
|
||||
return {
|
||||
maxWidth: Math.max(0, screenWidth - titleInsetLeft - titleInsetRight),
|
||||
titleInsetLeft,
|
||||
titleInsetRight,
|
||||
};
|
||||
};
|
||||
|
||||
const buildIos26HeaderTitleLayoutOptions = (
|
||||
screenWidth: number,
|
||||
): Pick<WalletTransactionsScrolledHeaderOptions, 'headerTitleAlign' | 'headerTitleContainerStyle'> => ({
|
||||
headerTitleAlign: 'left',
|
||||
headerTitleContainerStyle: {
|
||||
width: screenWidth,
|
||||
maxWidth: screenWidth,
|
||||
alignSelf: 'flex-start',
|
||||
alignItems: 'flex-start',
|
||||
left: 0,
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
});
|
||||
|
||||
type WalletTransactionsScrolledHeaderTitleProps = {
|
||||
walletLabel: string;
|
||||
balance: string;
|
||||
};
|
||||
|
||||
type WalletTransactionsScrolledHeaderTitleAnimatedProps = WalletTransactionsScrolledHeaderTitleProps & {
|
||||
opacity: SharedValue<number>;
|
||||
};
|
||||
|
||||
const WalletTransactionsScrolledHeaderTitleAnimated: React.FC<WalletTransactionsScrolledHeaderTitleAnimatedProps> = ({
|
||||
opacity,
|
||||
walletLabel,
|
||||
balance,
|
||||
}) => {
|
||||
const { width: screenWidth } = useWindowDimensions();
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: opacity.value,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Animated.View style={[scrolledHeaderTitleStyles.animatedTitleWrapper, { width: screenWidth }, animatedStyle]} pointerEvents="box-none">
|
||||
<WalletTransactionsScrolledHeaderTitle walletLabel={walletLabel} balance={balance} />
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
const WalletTransactionsScrolledHeaderTitle: React.FC<WalletTransactionsScrolledHeaderTitleProps> = ({ walletLabel, balance }) => {
|
||||
const { width: screenWidth } = useWindowDimensions();
|
||||
const { colors } = useTheme();
|
||||
const { maxWidth, titleInsetLeft, titleInsetRight } = getScrolledHeaderTitleLayout(screenWidth);
|
||||
|
||||
const titleColor = Platform.OS === 'ios' ? colors.foregroundColor : '#FFFFFF';
|
||||
|
||||
const titleContent = (
|
||||
<>
|
||||
<Text style={[scrolledHeaderTitleStyles.walletLabel, { color: titleColor }]} numberOfLines={1} ellipsizeMode="tail">
|
||||
{walletLabel}
|
||||
</Text>
|
||||
{balance.length > 0 ? (
|
||||
<Text style={[scrolledHeaderTitleStyles.balance, { color: titleColor }]} numberOfLines={1} ellipsizeMode="tail">
|
||||
{balance}
|
||||
</Text>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
return (
|
||||
<View style={[scrolledHeaderTitleStyles.iosHeaderRoot, { width: screenWidth }]}>
|
||||
<View
|
||||
style={[
|
||||
scrolledHeaderTitleStyles.container,
|
||||
scrolledHeaderTitleStyles.iosTitleArea,
|
||||
{ left: titleInsetLeft, right: titleInsetRight },
|
||||
]}
|
||||
>
|
||||
{titleContent}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return <View style={[scrolledHeaderTitleStyles.container, { maxWidth }]}>{titleContent}</View>;
|
||||
};
|
||||
|
||||
type TransactionListItem = Transaction & { type: 'transaction' | 'header' };
|
||||
const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { route: WalletTransactionsRouteProps }) => {
|
||||
const { wallets, saveToDisk } = useStorage();
|
||||
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
|
||||
@ -188,11 +73,8 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
const [pageSize] = useState(20);
|
||||
const navigation = useExtendedNavigation();
|
||||
const { setOptions, navigate } = navigation;
|
||||
const { colors, dark } = useTheme();
|
||||
const { colors } = useTheme();
|
||||
const { isElectrumDisabled } = useSettings();
|
||||
const insets = useSafeAreaInsets();
|
||||
const navBarHeight = Platform.select({ ios: 44, android: 56, default: 44 }) ?? 44;
|
||||
const headerOverlayHeight = insets.top + navBarHeight;
|
||||
const walletActionButtonsRef = useRef<View>(null);
|
||||
const [lastFetchTimestamp, setLastFetchTimestamp] = useState(() => wallet._lastTxFetch || 0);
|
||||
const [fetchFailures, setFetchFailures] = useState(0);
|
||||
@ -205,8 +87,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
const MAX_FAILURES = 3;
|
||||
const flatListRef = useRef<FlatList<Transaction>>(null);
|
||||
const headerRef = useRef<View>(null);
|
||||
const headerScrolledRef = useRef(false);
|
||||
const scrolledHeaderOpacity = useSharedValue(0);
|
||||
const [headerHeight, setHeaderHeight] = useState(0);
|
||||
|
||||
const stylesHook = StyleSheet.create({
|
||||
listHeaderText: {
|
||||
@ -219,17 +100,44 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
backgroundContainer: {
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
gradientBackground: {
|
||||
backgroundColor: WalletGradient.headerColorFor(wallet.type),
|
||||
height: headerHeight > 0 ? headerHeight : '30%',
|
||||
},
|
||||
activityIndicatorStyle: {
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
sendIcon: {
|
||||
transform: [{ rotate: direction === 'rtl' ? '-225deg' : '225deg' }],
|
||||
},
|
||||
receiveIcon: {
|
||||
transform: [{ rotate: direction === 'rtl' ? '-45deg' : '45deg' }],
|
||||
sendIcon: { transform: [{ rotate: direction === 'rtl' ? '-225deg' : '225deg' }] },
|
||||
receiveIcon: { transform: [{ rotate: direction === 'rtl' ? '-45deg' : '45deg' }] },
|
||||
headerBottomBar: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 12,
|
||||
height: 12,
|
||||
backgroundColor: colors.background,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
shadowColor: colors.shadowColor,
|
||||
shadowOffset: { width: 0, height: -8 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 6,
|
||||
},
|
||||
android: {
|
||||
elevation: 0.5,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setOptions(getWalletTransactionsOptions({ route }));
|
||||
}, [route, setOptions]),
|
||||
);
|
||||
|
||||
const onBarCodeRead = useCallback(
|
||||
(ret?: { data?: any }) => {
|
||||
if (!isLoading) {
|
||||
@ -239,15 +147,9 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
uri: ret?.data ? ret.data : ret,
|
||||
};
|
||||
if (wallet.chain === Chain.ONCHAIN) {
|
||||
navigate('SendDetailsRoot', {
|
||||
screen: 'SendDetails',
|
||||
params: parameters,
|
||||
});
|
||||
navigate('SendDetailsRoot', { screen: 'SendDetails', params: parameters });
|
||||
} else {
|
||||
navigate('ScanLNDInvoiceRoot', {
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: parameters,
|
||||
});
|
||||
navigate('ScanLNDInvoiceRoot', { screen: 'ScanLNDInvoice', params: parameters });
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
@ -265,6 +167,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
|
||||
useEffect(() => {
|
||||
// keep local display unit in sync when wallet changes (e.g., switching wallets)
|
||||
console.debug('[UnitSwitch] sync from wallet preferred unit', { walletID, preferred: wallet.preferredBalanceUnit });
|
||||
setDisplayUnit(wallet.preferredBalanceUnit);
|
||||
}, [wallet, walletID]);
|
||||
|
||||
@ -273,6 +176,10 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [walletID]);
|
||||
|
||||
useEffect(() => {
|
||||
console.debug('[UnitSwitch] display unit state changed', { walletID, displayUnit, switching: isUnitSwitching });
|
||||
}, [walletID, displayUnit, isUnitSwitching]);
|
||||
|
||||
const sortedTransactions = useMemo(() => {
|
||||
const txs = wallet.getTransactions();
|
||||
txs.sort((a, b) => b.timestamp - a.timestamp);
|
||||
@ -396,10 +303,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
await wallet.fetchBtcAddress();
|
||||
toAddress = wallet.refill_addressess[0];
|
||||
} catch (Err) {
|
||||
return presentAlert({
|
||||
message: (Err as Error).message,
|
||||
type: AlertType.Toast,
|
||||
});
|
||||
return presentAlert({ message: (Err as Error).message, type: AlertType.Toast });
|
||||
}
|
||||
}
|
||||
|
||||
@ -438,17 +342,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
|
||||
@ -493,10 +391,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
|
||||
const sendButtonPress = () => {
|
||||
if (wallet.chain === Chain.OFFCHAIN) {
|
||||
return navigate('ScanLNDInvoiceRoot', {
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: { walletID },
|
||||
});
|
||||
return navigate('ScanLNDInvoiceRoot', { screen: 'ScanLNDInvoice', params: { walletID } });
|
||||
}
|
||||
|
||||
if (wallet.type === WatchOnlyWallet.type && wallet.isHd() && !wallet.useWithHardwareWalletEnabled()) {
|
||||
@ -598,136 +493,55 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [wallet, wallet.hideBalance, displayUnit, balance]);
|
||||
|
||||
const walletLabel = wallet.getLabel();
|
||||
const scrolledHeaderTitle = useCallback(() => {
|
||||
if (usesIos26AnimatedScrolledHeader) {
|
||||
return (
|
||||
<WalletTransactionsScrolledHeaderTitleAnimated opacity={scrolledHeaderOpacity} walletLabel={walletLabel} balance={walletBalance} />
|
||||
);
|
||||
}
|
||||
return <WalletTransactionsScrolledHeaderTitle walletLabel={walletLabel} balance={walletBalance} />;
|
||||
}, [walletLabel, walletBalance, scrolledHeaderOpacity]);
|
||||
|
||||
const { width: screenWidth } = useWindowDimensions();
|
||||
|
||||
const getScrolledHeaderOptions = useCallback((): WalletTransactionsScrolledHeaderOptions => {
|
||||
const { titleInsetRight } = getScrolledHeaderTitleLayout(screenWidth);
|
||||
const routeIsLoading = route.params.isLoading ?? false;
|
||||
const scrolledHeaderIconColor = colors.foregroundColor;
|
||||
|
||||
return {
|
||||
headerTitle: scrolledHeaderTitle,
|
||||
// iOS ignores 'left'; title is positioned manually in WalletTransactionsScrolledHeaderTitle.
|
||||
...(Platform.OS === 'ios'
|
||||
? buildIos26HeaderTitleLayoutOptions(screenWidth)
|
||||
: {
|
||||
headerTitleAlign: 'left' as const,
|
||||
headerTitleContainerStyle: {
|
||||
paddingRight: titleInsetRight,
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: WalletGradient.headerColorFor(wallet.type),
|
||||
},
|
||||
headerTintColor: '#ffffff',
|
||||
}),
|
||||
...(Platform.OS === 'ios'
|
||||
? {
|
||||
headerTintColor: scrolledHeaderIconColor,
|
||||
statusBarStyle: 'light',
|
||||
...(isIOS26OrHigher && !isDesktop
|
||||
? {
|
||||
headerRight: undefined,
|
||||
unstable_headerRightItems: createWalletDetailsHeaderRightItems({
|
||||
isLoading: routeIsLoading,
|
||||
walletID,
|
||||
}),
|
||||
experimental_userInterfaceStyle: dark ? ('dark' as const) : ('light' as const),
|
||||
}
|
||||
: {
|
||||
headerBlurEffect: dark ? ('dark' as const) : ('light' as const),
|
||||
headerRight: createWalletDetailsHeaderRight({
|
||||
walletID,
|
||||
isLoading: routeIsLoading,
|
||||
iconColor: scrolledHeaderIconColor,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}, [scrolledHeaderTitle, screenWidth, colors.foregroundColor, dark, route.params.isLoading, walletID, wallet.type]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!headerScrolledRef.current) return;
|
||||
setOptions(getScrolledHeaderOptions());
|
||||
}, [walletBalance, getScrolledHeaderOptions, setOptions]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (usesIos26AnimatedScrolledHeader) {
|
||||
headerScrolledRef.current = false;
|
||||
scrolledHeaderOpacity.value = 0;
|
||||
setOptions({
|
||||
...getWalletTransactionsOptions({ route }),
|
||||
...buildIos26HeaderTitleLayoutOptions(screenWidth),
|
||||
headerTitle: scrolledHeaderTitle,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setOptions(getWalletTransactionsOptions({ route }));
|
||||
}, [route, screenWidth, scrolledHeaderTitle, scrolledHeaderOpacity, setOptions]),
|
||||
);
|
||||
|
||||
const handleScroll = useCallback(
|
||||
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
(event: any) => {
|
||||
const offsetY = event.nativeEvent.contentOffset.y;
|
||||
const scrolled = offsetY >= SCROLLED_HEADER_SHOW_OFFSET;
|
||||
|
||||
if (usesIos26AnimatedScrolledHeader) {
|
||||
if (scrolled === headerScrolledRef.current) return;
|
||||
headerScrolledRef.current = scrolled;
|
||||
scrolledHeaderOpacity.value = withTiming(scrolled ? 1 : 0, {
|
||||
duration: scrolled ? SCROLLED_HEADER_FADE_IN_MS : SCROLLED_HEADER_FADE_OUT_MS,
|
||||
});
|
||||
if (scrolled) {
|
||||
setOptions(getScrolledHeaderOptions());
|
||||
} else {
|
||||
setOptions({
|
||||
...getWalletTransactionsOptions({ route }),
|
||||
...buildIos26HeaderTitleLayoutOptions(screenWidth),
|
||||
headerTitle: scrolledHeaderTitle,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrolled === headerScrolledRef.current) return;
|
||||
headerScrolledRef.current = scrolled;
|
||||
|
||||
if (!scrolled) {
|
||||
setOptions({
|
||||
...getWalletTransactionsOptions({ route }),
|
||||
headerTitle: undefined,
|
||||
headerTitleAlign: undefined,
|
||||
headerTitleContainerStyle: undefined,
|
||||
headerBlurEffect: undefined,
|
||||
});
|
||||
const combinedHeight = 180;
|
||||
if (offsetY < combinedHeight) {
|
||||
setOptions({ ...getWalletTransactionsOptions({ route }), headerTitle: undefined });
|
||||
} else {
|
||||
setOptions(getScrolledHeaderOptions());
|
||||
navigation.setOptions({
|
||||
headerTitle: `${wallet.getLabel()} ${walletBalance}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
[getScrolledHeaderOptions, setOptions, route, screenWidth, scrolledHeaderTitle, scrolledHeaderOpacity],
|
||||
[navigation, wallet, walletBalance, setOptions, route],
|
||||
);
|
||||
|
||||
const ListHeaderComponent = useCallback(
|
||||
const measureHeaderHeight = useCallback(() => {
|
||||
if (!headerRef.current) {
|
||||
// If header ref is not available, use default background
|
||||
setHeaderHeight(0);
|
||||
return;
|
||||
}
|
||||
|
||||
headerRef.current.measure((x, y, width, height, pageX, pageY) => {
|
||||
// Check if the header is actually visible
|
||||
if (height === 0 || pageY < 0) {
|
||||
// Header is not visible, use default background
|
||||
setHeaderHeight(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const fullHeight = pageY + height;
|
||||
if (fullHeight > 0) {
|
||||
setHeaderHeight(fullHeight);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(measureHeaderHeight, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, [walletID, measureHeaderHeight]);
|
||||
|
||||
const ListHeaderComponent = useMemo(
|
||||
() => (
|
||||
<View ref={headerRef}>
|
||||
<View ref={headerRef} onLayout={measureHeaderHeight}>
|
||||
<TransactionsNavigationHeader
|
||||
headerOverlayHeight={headerOverlayHeight}
|
||||
wallet={wallet}
|
||||
onWalletUnitChange={async selectedUnit => {
|
||||
console.debug('[UnitSwitch] requested', { walletID, from: displayUnit, to: selectedUnit });
|
||||
setIsUnitSwitching(true);
|
||||
setDisplayUnit(selectedUnit);
|
||||
if ('setPreferredBalanceUnit' in wallet) {
|
||||
@ -736,25 +550,22 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
(wallet as TWallet).preferredBalanceUnit = selectedUnit;
|
||||
}
|
||||
await saveToDisk();
|
||||
console.debug('[UnitSwitch] persisted preferred unit', { walletID, unit: selectedUnit });
|
||||
setTimeout(() => {
|
||||
setIsUnitSwitching(false);
|
||||
console.debug('[UnitSwitch] complete', { walletID, unit: selectedUnit });
|
||||
}, 50);
|
||||
}}
|
||||
unit={displayUnit}
|
||||
unitSwitching={isUnitSwitching}
|
||||
onWalletBalanceVisibilityChange={async shouldHideBalance => {
|
||||
try {
|
||||
const isBiometricsEnabled = await isBiometricUseCapableAndEnabled();
|
||||
if (wallet.hideBalance && !shouldHideBalance && isBiometricsEnabled) {
|
||||
if (!(await unlockWithBiometrics())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
wallet.hideBalance = shouldHideBalance;
|
||||
await saveToDisk();
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle balance visibility:', error);
|
||||
onWalletBalanceVisibilityChange={async isShouldBeVisible => {
|
||||
const isBiometricsEnabled = await isBiometricUseCapableAndEnabled();
|
||||
if (wallet.hideBalance && isBiometricsEnabled) {
|
||||
const unlocked = await unlockWithBiometrics();
|
||||
if (!unlocked) throw new Error('Biometrics failed');
|
||||
}
|
||||
wallet.hideBalance = isShouldBeVisible;
|
||||
await saveToDisk();
|
||||
}}
|
||||
onManageFundsPressed={id => {
|
||||
if (wallet.type === MultisigHDWallet.type) {
|
||||
@ -780,30 +591,36 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<View style={[styles.flex, styles.transactionsSection, stylesHook.backgroundContainer]}>
|
||||
<View style={styles.listHeaderTextRow}>
|
||||
<Text style={[styles.listHeaderText, stylesHook.listHeaderText]}>{loc.transactions.list_title}</Text>
|
||||
<View style={styles.headerBottomBarSpacer}>
|
||||
<View style={stylesHook.headerBottomBar} />
|
||||
</View>
|
||||
<>
|
||||
<View style={[styles.flex, stylesHook.backgroundContainer]}>
|
||||
<View style={styles.listHeaderTextRow}>
|
||||
<Text style={[styles.listHeaderText, stylesHook.listHeaderText]}>{loc.transactions.list_title}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={stylesHook.backgroundContainer}>
|
||||
{wallet.type === WatchOnlyWallet.type && isWatchOnlyWarningVisible && (
|
||||
<WatchOnlyWarning
|
||||
handleDismiss={() => {
|
||||
setIsWatchOnlyWarningVisible(false);
|
||||
wallet.isWatchOnlyWarningVisible = false;
|
||||
saveToDisk();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View style={stylesHook.backgroundContainer}>
|
||||
{wallet.type === WatchOnlyWallet.type && isWatchOnlyWarningVisible && (
|
||||
<WatchOnlyWarning
|
||||
handleDismiss={() => {
|
||||
setIsWatchOnlyWarningVisible(false);
|
||||
wallet.isWatchOnlyWarningVisible = false;
|
||||
saveToDisk();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
</View>
|
||||
),
|
||||
[
|
||||
wallet,
|
||||
displayUnit,
|
||||
isUnitSwitching,
|
||||
headerOverlayHeight,
|
||||
measureHeaderHeight,
|
||||
stylesHook.backgroundContainer,
|
||||
stylesHook.headerBottomBar,
|
||||
stylesHook.listHeaderText,
|
||||
saveToDisk,
|
||||
isBiometricUseCapableAndEnabled,
|
||||
@ -816,18 +633,16 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
headerScrolledRef.current = false;
|
||||
scrolledHeaderOpacity.value = 0;
|
||||
if (flatListRef.current) {
|
||||
flatListRef.current.scrollToOffset({ offset: 0, animated: true });
|
||||
}
|
||||
}, [walletID, scrolledHeaderOpacity]);
|
||||
}, [walletID]);
|
||||
|
||||
return (
|
||||
<View style={[styles.flex, { backgroundColor: WalletGradient.headerColorFor(wallet.type) }]} testID="TransactionsListView">
|
||||
<View style={[styles.flex, stylesHook.backgroundContainer]}>
|
||||
<View style={[styles.refreshIndicatorBackground, stylesHook.gradientBackground]} testID="TransactionsListView" />
|
||||
<FlatList<Transaction>
|
||||
ref={flatListRef}
|
||||
style={styles.flatList}
|
||||
getItemLayout={getItemLayout}
|
||||
updateCellsBatchingPeriod={50}
|
||||
onEndReachedThreshold={0.3}
|
||||
@ -838,9 +653,8 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
keyExtractor={_keyExtractor}
|
||||
renderItem={renderItem}
|
||||
initialNumToRender={10}
|
||||
removeClippedSubviews={false}
|
||||
contentContainerStyle={[styles.contentContainer, stylesHook.backgroundContainer]}
|
||||
contentInsetAdjustmentBehavior="never"
|
||||
removeClippedSubviews
|
||||
contentContainerStyle={stylesHook.backgroundContainer}
|
||||
contentInset={{ top: 0, left: 0, bottom: 90, right: 0 }}
|
||||
maxToRenderPerBatch={10}
|
||||
onScroll={handleScroll}
|
||||
@ -857,25 +671,11 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
}
|
||||
refreshControl={
|
||||
!isDesktop && !isElectrumDisabled ? (
|
||||
<RefreshControl
|
||||
refreshing={isLoading}
|
||||
onRefresh={() => refreshTransactions(true)}
|
||||
tintColor={Platform.OS === 'ios' ? 'transparent' : colors.msSuccessCheck}
|
||||
progressViewOffset={headerOverlayHeight}
|
||||
/>
|
||||
<RefreshControl refreshing={isLoading} onRefresh={() => refreshTransactions(true)} tintColor={colors.msSuccessCheck} />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{isLoading && Platform.OS === 'ios' && (
|
||||
<ActivityIndicator
|
||||
style={[styles.refreshSpinner, { top: headerOverlayHeight + 12, transform: [{ scale: 1.4 }] }]}
|
||||
color="#ffffff"
|
||||
size="small"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
)}
|
||||
|
||||
<FloatButtonsBottomFade />
|
||||
<FContainer ref={walletActionButtonsRef}>
|
||||
{wallet.allowReceive() && (
|
||||
@ -884,10 +684,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
text={loc.receive.header}
|
||||
onPress={() => {
|
||||
if (wallet.chain === Chain.OFFCHAIN) {
|
||||
navigate('LNDCreateInvoiceRoot', {
|
||||
screen: 'LNDCreateInvoice',
|
||||
params: { walletID },
|
||||
});
|
||||
navigate('LNDCreateInvoiceRoot', { screen: 'LNDCreateInvoice', params: { walletID } });
|
||||
} else {
|
||||
navigate('ReceiveDetails', { walletID });
|
||||
}
|
||||
@ -938,81 +735,22 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
|
||||
export default WalletTransactions;
|
||||
|
||||
const scrolledHeaderTitleStyles = StyleSheet.create({
|
||||
animatedTitleWrapper: {
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
iosHeaderRoot: {
|
||||
height: 44,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
iosTitleArea: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
minWidth: 0,
|
||||
},
|
||||
container: {
|
||||
minWidth: 0,
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
walletLabel: {
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.15,
|
||||
alignSelf: 'stretch',
|
||||
flexShrink: 1,
|
||||
},
|
||||
balance: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
lineHeight: 18,
|
||||
marginTop: 1,
|
||||
alignSelf: 'stretch',
|
||||
flexShrink: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: { flex: 1 },
|
||||
flatList: { flex: 1, backgroundColor: 'transparent' },
|
||||
transactionsSection: { marginTop: -1 },
|
||||
scrollViewContent: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 500,
|
||||
},
|
||||
headerBottomBarSpacer: { position: 'relative', height: 12 },
|
||||
scrollViewContent: { flex: 1, justifyContent: 'center', paddingHorizontal: 16, paddingBottom: 500 },
|
||||
activityIndicator: { marginVertical: 20 },
|
||||
listHeaderTextRow: {
|
||||
flex: 1,
|
||||
marginHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
listHeaderTextRow: { flex: 1, marginHorizontal: 16, flexDirection: 'row', justifyContent: 'space-between' },
|
||||
listHeaderText: { marginTop: 0, marginBottom: 16, fontWeight: 'bold', fontSize: 24 },
|
||||
refreshIndicatorBackground: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
listHeaderText: {
|
||||
marginTop: 16,
|
||||
marginBottom: 16,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 24,
|
||||
},
|
||||
contentContainer: { flexGrow: 1 },
|
||||
refreshSpinner: { position: 'absolute', alignSelf: 'center', zIndex: 10 },
|
||||
emptyTxsContainer: { height: '10%', minHeight: '10%', flex: 1 },
|
||||
emptyTxs: {
|
||||
fontSize: 18,
|
||||
color: '#9aa0aa',
|
||||
textAlign: 'center',
|
||||
marginVertical: 16,
|
||||
},
|
||||
emptyTxsLightning: {
|
||||
fontSize: 18,
|
||||
color: '#9aa0aa',
|
||||
textAlign: 'center',
|
||||
fontWeight: '600',
|
||||
},
|
||||
emptyTxs: { fontSize: 18, color: '#9aa0aa', textAlign: 'center', marginVertical: 16 },
|
||||
emptyTxsLightning: { fontSize: 18, color: '#9aa0aa', textAlign: 'center', fontWeight: '600' },
|
||||
iconContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
|
||||
@ -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';
|
||||
@ -30,10 +25,8 @@ import { useSettings } from '../../hooks/context/useSettings';
|
||||
import useMenuElements from '../../hooks/useMenuElements';
|
||||
import SafeAreaSectionList from '../../components/SafeAreaSectionList';
|
||||
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 +107,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 +123,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),
|
||||
},
|
||||
});
|
||||
|
||||
@ -484,9 +471,7 @@ const WalletsList: React.FC = () => {
|
||||
|
||||
const sectionListKeyExtractor = useCallback((item: any, index: any) => {
|
||||
if (typeof item === 'string') return item;
|
||||
const txKey = item?.hash || item?.txid;
|
||||
if (txKey && item?.walletID) return `${txKey}_${item.walletID}`;
|
||||
return txKey || `${item}${index}`;
|
||||
return item?.hash || item?.txid || `${item}${index}`;
|
||||
}, []);
|
||||
|
||||
const refreshProps = isDesktop || isElectrumDisabled ? {} : { refreshing: isLoading, onRefresh };
|
||||
@ -505,9 +490,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 +506,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 +515,7 @@ const WalletsList: React.FC = () => {
|
||||
// First section: Carousel
|
||||
if (index === 0) {
|
||||
return {
|
||||
length: carouselHeight,
|
||||
length: CAROUSEL_HEIGHT,
|
||||
offset: 0,
|
||||
index,
|
||||
};
|
||||
@ -538,13 +528,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,13 +547,11 @@ const WalletsList: React.FC = () => {
|
||||
initialNumToRender={10}
|
||||
renderSectionFooter={renderSectionFooter}
|
||||
sections={sections}
|
||||
floatingButtonHeight={floatingButtonHeight}
|
||||
floatingButtonHeight={70}
|
||||
maxToRenderPerBatch={10}
|
||||
updateCellsBatchingPeriod={50}
|
||||
getItemLayout={getItemLayout}
|
||||
ignoreTopInset={true} // Ignore top inset as the screen header already handles it
|
||||
// On iOS 26+, let the section headers scroll naturally with the content rather than sticking
|
||||
stickySectionHeadersEnabled={!isIOS26OrHigher}
|
||||
{...refreshProps}
|
||||
/>
|
||||
{renderScanButton()}
|
||||
|
||||
@ -4,7 +4,6 @@ import { element, waitFor } from 'detox';
|
||||
|
||||
import {
|
||||
confirmPasswordDialog,
|
||||
dismissAlertByText,
|
||||
expectToBeVisible,
|
||||
extractTextFromElementById,
|
||||
goBack,
|
||||
@ -194,36 +193,31 @@ describe('BlueWallet UI Tests - no wallets', () => {
|
||||
await waitFor(element(by.id('NotificationsSwitch')))
|
||||
.toBeVisible()
|
||||
.withTimeout(10000);
|
||||
await element(by.id('NotificationsSwitch')).tap();
|
||||
|
||||
// Toggle notifications on/off. On iOS 26 simulators notifications are always
|
||||
// denied, triggering a native UIAlertController whose buttons liquid glass
|
||||
// can make un-tappable by Detox. If the alert cannot be dismissed, relaunch
|
||||
// the app to recover instead of failing the entire settings test.
|
||||
let notifDialogStuck = false;
|
||||
// If notifications are not enabled on the device, an alert will appear
|
||||
try {
|
||||
await element(by.id('NotificationsSwitch')).tap();
|
||||
const dismissed1 = await dismissAlertByText('OK', 10000);
|
||||
if (dismissed1) {
|
||||
await sleep(500);
|
||||
await element(by.id('NotificationsSwitch')).tap();
|
||||
await dismissAlertByText('OK', 10000);
|
||||
} else {
|
||||
notifDialogStuck = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Notifications toggle skipped due to alert interaction issue:', e.message);
|
||||
notifDialogStuck = true;
|
||||
await waitFor(element(by.text('OK')))
|
||||
.toBeVisible()
|
||||
.withTimeout(3000);
|
||||
await element(by.text('OK')).tap();
|
||||
} catch (_) {
|
||||
// Alert not shown, which is fine - notifications might be enabled
|
||||
}
|
||||
await element(by.id('NotificationsSwitch')).tap();
|
||||
|
||||
// If notifications are not enabled on the device, an alert will appear
|
||||
try {
|
||||
await waitFor(element(by.text('OK')))
|
||||
.toBeVisible()
|
||||
.withTimeout(3000);
|
||||
await element(by.text('OK')).tap();
|
||||
} catch (_) {
|
||||
// Alert not shown, which is fine - notifications might be enabled
|
||||
}
|
||||
|
||||
if (notifDialogStuck) {
|
||||
// Dialog blocks all interaction; relaunch the app to clear it
|
||||
await device.launchApp({ newInstance: true });
|
||||
await waitForId('WalletsList');
|
||||
await element(by.id('SettingsButton')).tap();
|
||||
} else {
|
||||
await goBack();
|
||||
await goBack();
|
||||
}
|
||||
await goBack();
|
||||
await goBack();
|
||||
} else {
|
||||
await goBack();
|
||||
}
|
||||
|
||||
@ -58,20 +58,8 @@ export async function waitForText(text, timeout = 33000) {
|
||||
await waitFor(element(by.text(text)))
|
||||
.toBeVisible()
|
||||
.withTimeout(timeout / 2);
|
||||
return true;
|
||||
} catch (err) {
|
||||
// iOS 26 liquid glass: text rendered inside/over the glass header (e.g. the wallet name on
|
||||
// the transactions hero) can fail Detox's 75%-pixel toBeVisible check while still being
|
||||
// present and on-screen — same root cause as the goBack() back-button workaround. Fall back
|
||||
// to existence in the hierarchy so a glass false-negative does not fail an otherwise valid run.
|
||||
try {
|
||||
await waitFor(element(by.text(text)))
|
||||
.toExist()
|
||||
.withTimeout(3000);
|
||||
return true;
|
||||
} catch (_) {
|
||||
rethrowWithCallsite(err, callsite);
|
||||
}
|
||||
rethrowWithCallsite(err, callsite);
|
||||
}
|
||||
}
|
||||
|
||||
@ -228,44 +216,15 @@ export async function helperCreateWallet(walletName) {
|
||||
await element(by.id('ActivateBitcoinButton')).tap();
|
||||
await element(by.id('ActivateBitcoinButton')).tap();
|
||||
// why tf we need 2 taps for it to work..? mystery
|
||||
await tapAndTapAgainIfElementIsNotVisible('Create', 'PleaseBackupScrollView');
|
||||
|
||||
// iOS 26 liquid glass: the navigation transition after tapping "Create" triggers
|
||||
// glass animations that never fully settle, keeping the app in a "busy" state.
|
||||
// Detox synchronization waits for idle before proceeding, causing an infinite hang.
|
||||
// Disable sync for the remainder of wallet creation and re-enable once we're back
|
||||
// on the home screen where the glass animations have settled.
|
||||
const isIOS = device.getPlatform() === 'ios';
|
||||
if (isIOS) {
|
||||
await device.disableSynchronization();
|
||||
}
|
||||
try {
|
||||
await element(by.id('Create')).tap();
|
||||
await sleep(500);
|
||||
try {
|
||||
await waitFor(element(by.id('PleaseBackupScrollView')))
|
||||
.toBeVisible()
|
||||
.withTimeout(15000);
|
||||
} catch (_) {
|
||||
await element(by.id('Create')).tap();
|
||||
await sleep(500);
|
||||
await waitFor(element(by.id('PleaseBackupScrollView')))
|
||||
.toBeVisible()
|
||||
.withTimeout(15000);
|
||||
}
|
||||
await waitFor(element(by.id('PleasebackupOk')))
|
||||
.toBeVisible()
|
||||
.whileElement(by.id('PleaseBackupScrollView'))
|
||||
.scroll(500, 'down'); // in case emu screen is small and it doesnt fit
|
||||
|
||||
await waitFor(element(by.id('PleasebackupOk')))
|
||||
.toBeVisible()
|
||||
.whileElement(by.id('PleaseBackupScrollView'))
|
||||
.scroll(500, 'down'); // in case emu screen is small and it doesnt fit
|
||||
|
||||
await element(by.id('PleasebackupOk')).tap();
|
||||
await sleep(1000);
|
||||
await scrollUpOnHomeScreen();
|
||||
} finally {
|
||||
if (isIOS) {
|
||||
await device.enableSynchronization();
|
||||
}
|
||||
}
|
||||
await element(by.id('PleasebackupOk')).tap();
|
||||
await scrollUpOnHomeScreen();
|
||||
await expect(element(by.id('WalletsList'))).toBeVisible();
|
||||
await element(by.id('WalletsList')).swipe('right', 'fast', 1); // in case emu screen is small and it doesnt fit
|
||||
await sleep(200);
|
||||
@ -338,46 +297,6 @@ export async function tapIfTextPresent(text) {
|
||||
// no need to check for visibility, just silently ignore exception if such testID is not present
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses a native UIAlertController by tapping a button with the given text.
|
||||
* On iOS 26 liquid glass, `waitFor().toBeVisible()` never resolves for alert
|
||||
* buttons because the glass material fails Detox's pixel visibility check.
|
||||
* This helper disables Detox synchronization (which can also hang on glass
|
||||
* animations) and polls with direct tap attempts and label fallbacks.
|
||||
*
|
||||
* @returns true if the alert was dismissed, false if no alert was found
|
||||
*/
|
||||
export async function dismissAlertByText(text, timeoutMs = 10000) {
|
||||
const isIOS = device.getPlatform() === 'ios';
|
||||
if (isIOS) {
|
||||
await device.disableSynchronization();
|
||||
}
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let dismissed = false;
|
||||
try {
|
||||
while (Date.now() < deadline) {
|
||||
// by.text — works on pre–iOS 26 and some iOS 26 alerts
|
||||
try {
|
||||
await element(by.text(text)).atIndex(0).tap();
|
||||
dismissed = true;
|
||||
break;
|
||||
} catch (_) {}
|
||||
// by.label — accessibility label, works when text matching differs
|
||||
try {
|
||||
await element(by.label(text)).atIndex(0).tap();
|
||||
dismissed = true;
|
||||
break;
|
||||
} catch (_) {}
|
||||
await sleep(500);
|
||||
}
|
||||
} finally {
|
||||
if (isIOS) {
|
||||
await device.enableSynchronization();
|
||||
}
|
||||
}
|
||||
return dismissed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms password dialogs in a platform-safe way.
|
||||
* Android must tap a visible confirmation to keep test flow deterministic.
|
||||
@ -454,14 +373,8 @@ export async function goBack() {
|
||||
// and when a modal covers a stack that also has a back button, the covered
|
||||
// one can precede the visible one in match order (seen with Reduce Motion on).
|
||||
// Probe attributes and only tap an element detox reports as visible & hittable.
|
||||
//
|
||||
// iOS 26 liquid glass: the native back button reports visible=false because
|
||||
// the glass material fails Detox's 75%-pixel visibility check, yet the button
|
||||
// IS functionally hittable. We first try (visible && hittable), then fall back
|
||||
// to (hittable only) for the glass case.
|
||||
let lastErr;
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
// Pass 1: prefer visible + hittable elements
|
||||
for (const matcher of candidates) {
|
||||
for (let idx = 0; idx < 6; idx++) {
|
||||
let attrs;
|
||||
@ -480,25 +393,6 @@ export async function goBack() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pass 2: accept hittable-only elements (iOS 26 liquid glass back button)
|
||||
for (const matcher of candidates) {
|
||||
for (let idx = 0; idx < 6; idx++) {
|
||||
let attrs;
|
||||
try {
|
||||
attrs = await element(matcher).atIndex(idx).getAttributes();
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
break;
|
||||
}
|
||||
if (attrs.hittable === false) continue;
|
||||
try {
|
||||
await element(matcher).atIndex(idx).tap();
|
||||
return;
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
}
|
||||
}
|
||||
}
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
|
||||
@ -43,19 +43,4 @@ describe('unit - encryption', function () {
|
||||
const decrypted = c.decrypt(crypted, 'password');
|
||||
assert.deepEqual(data2decrypt, decrypted);
|
||||
});
|
||||
|
||||
it('can decrypt a ciphertext produced by the OpenSSL CLI (wire-format check)', () => {
|
||||
// Regenerate this fixture with (copy-pasteable, verified to reproduce the byte string below):
|
||||
//
|
||||
// { printf 'Salted__\x01\x02\x03\x04\x05\x06\x07\x08'; \
|
||||
// printf 'hello world this is plaintext' \
|
||||
// | openssl enc -aes-256-cbc -k mypassword -S 0102030405060708 -md md5; \
|
||||
// } | base64
|
||||
//
|
||||
// OpenSSL's `enc` only emits the `Salted__` envelope when it picks the salt itself;
|
||||
// passing `-S <hex>` suppresses the header, so we prepend it manually. Pins the
|
||||
// on-disk format against an independent reference beyond crypto-js.
|
||||
const crypted = 'U2FsdGVkX18BAgMEBQYHCMqtJuZaneiHrVN/oMPPLvFplovZbI1K+lulGJn7NAvn';
|
||||
assert.strictEqual(c.decrypt(crypted, 'mypassword'), 'hello world this is plaintext');
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import { evpBytesToKeyMd5 } from '../../blue_modules/encryption';
|
||||
import { hexToUint8Array, stringToUint8Array, uint8ArrayToHex } from '../../blue_modules/uint8array-extras';
|
||||
|
||||
describe('evpBytesToKeyMd5', () => {
|
||||
// Vectors computed against the OpenSSL EVP_BytesToKey reference algorithm
|
||||
// (MD5, 1 iteration). The KDF is purely deterministic, so a single fixed
|
||||
// (password, salt) pair pins the bytes our wallet store relies on.
|
||||
it('matches the OpenSSL CLI reference for password="mypassword"', () => {
|
||||
// openssl enc -aes-256-cbc -k mypassword -S 0102030405060708 -md md5 -p
|
||||
const out = evpBytesToKeyMd5(stringToUint8Array('mypassword'), hexToUint8Array('0102030405060708'), 48);
|
||||
assert.strictEqual(uint8ArrayToHex(out.subarray(0, 32)), '20814c3ad75ac1d26c61a8e4702b5ff4d7baaee00c595bab71592aaf45bf41e4');
|
||||
assert.strictEqual(uint8ArrayToHex(out.subarray(32, 48)), '43269499cb6d59f4e3b9dda68098b673');
|
||||
});
|
||||
|
||||
it('matches a Node-crypto reference vector for a multi-word password', () => {
|
||||
const out = evpBytesToKeyMd5(stringToUint8Array('correct horse'), hexToUint8Array('0102030405060708'), 48);
|
||||
assert.strictEqual(uint8ArrayToHex(out.subarray(0, 32)), 'bcf8d941d9291141709c9d56360eb7148e3960ab3dc44d832c4028568545c91d');
|
||||
assert.strictEqual(uint8ArrayToHex(out.subarray(32, 48)), '5a7a1d12207f801d2f6f4cf578e8708c');
|
||||
});
|
||||
|
||||
it('returns exactly the requested number of bytes', () => {
|
||||
const pwd = stringToUint8Array('pw');
|
||||
const salt = hexToUint8Array('00000000000000ff');
|
||||
assert.strictEqual(evpBytesToKeyMd5(pwd, salt, 1).length, 1);
|
||||
assert.strictEqual(evpBytesToKeyMd5(pwd, salt, 15).length, 15);
|
||||
assert.strictEqual(evpBytesToKeyMd5(pwd, salt, 16).length, 16); // one MD5 block exactly
|
||||
assert.strictEqual(evpBytesToKeyMd5(pwd, salt, 17).length, 17); // one block + 1
|
||||
assert.strictEqual(evpBytesToKeyMd5(pwd, salt, 48).length, 48); // key + iv default
|
||||
assert.strictEqual(evpBytesToKeyMd5(pwd, salt, 65).length, 65); // multi-block + spillover
|
||||
});
|
||||
|
||||
it('is a prefix-stable stream (same first N bytes regardless of total length)', () => {
|
||||
const pwd = stringToUint8Array('xyz');
|
||||
const salt = hexToUint8Array('cafebabedeadbeef');
|
||||
const long = evpBytesToKeyMd5(pwd, salt, 64);
|
||||
for (const n of [1, 16, 17, 32, 48]) {
|
||||
assert.strictEqual(uint8ArrayToHex(evpBytesToKeyMd5(pwd, salt, n)), uint8ArrayToHex(long.subarray(0, n)));
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects non-integer or negative byteLength', () => {
|
||||
const pwd = stringToUint8Array('pw');
|
||||
const salt = hexToUint8Array('0102030405060708');
|
||||
assert.throws(() => evpBytesToKeyMd5(pwd, salt, -1));
|
||||
assert.throws(() => evpBytesToKeyMd5(pwd, salt, 1.5));
|
||||
assert.throws(() => evpBytesToKeyMd5(pwd, salt, NaN));
|
||||
assert.strictEqual(evpBytesToKeyMd5(pwd, salt, 0).length, 0);
|
||||
});
|
||||
});
|
||||
@ -1,10 +0,0 @@
|
||||
{
|
||||
"xfp": "B68AF6E4",
|
||||
"account": 0,
|
||||
"p2wsh_deriv": "m/48h/0h/0h/2h",
|
||||
"p2wsh": "Zpub74w9dfoeurKrKXE3SPRpFquLPTkiCuSwGuhDzBgbE42w5ShB2FxMjmJyjZpSJ6WhLt8y1PeFHQELGgq2GmktviFDH8yFWYRWg4xQiw3v335",
|
||||
"p2sh_deriv": "m/45h",
|
||||
"p2sh": "xpub69EKPNo9Jkd6v2h7xNKw5RdbFBoaHEcstXcRNfcQ2jg71iFpobCwcxfJjaV2ycGy218f2jM1znqs1SDkqMiR7fbyBVJwzacg2QarGt1gtJg",
|
||||
"p2sh_p2wsh_deriv": "m/48h/0h/0h/1h",
|
||||
"p2sh_p2wsh": "Ypub6k6tL18jmAnNRGZpk4u3WPGDmWMkdZNmx3MySYdQywCwMMHqNoKHeqLAgU6pFokHKQFdi88vAW4g3TEsCAymoq5LnFXd54RkQ8m3AD9f81J"
|
||||
}
|
||||
@ -1160,7 +1160,6 @@ describe('LightningArkWallet — addInvoice + payInvoice (mocked SDK runtime)',
|
||||
// Real BOLT11 with amount = 0.0001 BTC (10000 sat) so it passes the limits assertion.
|
||||
const invoice =
|
||||
'lnbc100u1p50528cpp5rhy4fgs0ff23asecxtxt9zvc3apn0p8h7fxsj0d5k7j3x92zwhlqdq5w3jhxapqd9h8vmmfvdjscqrp80xqyf8ucsp5vcsrzye432n9wh0zwuv5z8y5n9zvkwpctr685e80utzc2yueccms9qxpqysgqd87swq3hput9k6llp0wxg098hc7ge3e5nrtnvak6zreywzaf4k9s8d3u4hrmt3m22kf0jt7ruqj0caknk5ykzdenjdphz50t7xrstnqqn6aw0m';
|
||||
const expectedPaymentHash = w.decodeInvoice(invoice).payment_hash;
|
||||
fakeArkadeSwaps.sendLightningPayment.mockResolvedValue({ amount: 10_000, preimage: 'pre', txid: 'tx' });
|
||||
|
||||
await w.payInvoice(invoice);
|
||||
@ -1168,11 +1167,6 @@ describe('LightningArkWallet — addInvoice + payInvoice (mocked SDK runtime)',
|
||||
assert.strictEqual(fakeArkadeSwaps.sendLightningPayment.mock.calls.length, 1);
|
||||
assert.strictEqual(fakeArkadeSwaps.sendLightningPayment.mock.calls[0][0].invoice, invoice);
|
||||
assert.strictEqual(fakeWallet.sendBitcoin.mock.calls.length, 0, 'Ark sendBitcoin must not run for BOLT11');
|
||||
assert.deepStrictEqual(w.last_paid_invoice_result, {
|
||||
payment_preimage: 'pre',
|
||||
payment_hash: expectedPaymentHash,
|
||||
payment_request: invoice,
|
||||
});
|
||||
});
|
||||
|
||||
it('payInvoice routes a valid Ark address through Wallet.sendBitcoin', async () => {
|
||||
|
||||
@ -208,17 +208,6 @@ describe('LNURL', function () {
|
||||
|
||||
assert.strictEqual(Lnurl.decipherAES(ciphertext, preimage, iv), '1234');
|
||||
});
|
||||
|
||||
it('decipherAES returns empty string on malformed input (preserves crypto-js contract)', () => {
|
||||
const preimage = 'bf62911aa53c017c27ba34391f694bc8bf8aaf59b4ebfd9020e66ac0412e189b';
|
||||
const validIv = 'eTGduB45hWTOxHj1dR+LJw==';
|
||||
// Non-block-aligned ciphertext — would throw under raw @noble/ciphers
|
||||
assert.strictEqual(Lnurl.decipherAES('not-base64-aligned', preimage, validIv), '');
|
||||
// Bad PKCS7 padding (random 16-byte block won't unpad cleanly)
|
||||
assert.strictEqual(Lnurl.decipherAES('AAAAAAAAAAAAAAAAAAAAAA==', preimage, validIv), '');
|
||||
// Empty ciphertext
|
||||
assert.strictEqual(Lnurl.decipherAES('', preimage, validIv), '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('lightning address', function () {
|
||||
|
||||
@ -2161,31 +2161,6 @@ describe('multisig-cosigner', () => {
|
||||
assert.strictEqual(c3.getPath(), "m/48'/0'/0'/2'");
|
||||
});
|
||||
|
||||
it('can parse unchained json', () => {
|
||||
const unchainedJson = require('./fixtures/unchained.json');
|
||||
|
||||
const cosigner = new MultisigCosigner(JSON.stringify(unchainedJson));
|
||||
assert.ok(cosigner.isValid());
|
||||
assert.strictEqual(cosigner.howManyCosignersWeHave(), 3);
|
||||
assert.strictEqual(cosigner.getFp(), '');
|
||||
assert.strictEqual(cosigner.getXpub(), '');
|
||||
assert.strictEqual(cosigner.getPath(), '');
|
||||
|
||||
const [c1, c2, c3] = cosigner.getAllCosigners();
|
||||
|
||||
assert.strictEqual(c1.getXpub(), unchainedJson.p2sh);
|
||||
assert.strictEqual(c1.getFp(), 'B68AF6E4');
|
||||
assert.strictEqual(c1.getPath(), "m/45'");
|
||||
|
||||
assert.strictEqual(c2.getXpub(), unchainedJson.p2sh_p2wsh);
|
||||
assert.strictEqual(c2.getFp(), 'B68AF6E4');
|
||||
assert.strictEqual(c2.getPath(), "m/48'/0'/0'/1'");
|
||||
|
||||
assert.strictEqual(c3.getXpub(), unchainedJson.p2wsh);
|
||||
assert.strictEqual(c3.getFp(), 'B68AF6E4');
|
||||
assert.strictEqual(c3.getPath(), "m/48'/0'/0'/2'");
|
||||
});
|
||||
|
||||
it('can parse plain Zpub', () => {
|
||||
const cosigner = new MultisigCosigner(Zpub1);
|
||||
assert.ok(cosigner.isValid());
|
||||
|
||||
Loading…
Reference in New Issue
Block a user