Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32d3f77f4f | ||
|
|
f26ff9189c | ||
|
|
1fa290652c | ||
|
|
099f6f46a6 | ||
|
|
01a11bc8dd | ||
|
|
6639891c24 | ||
|
|
4029d294f8 | ||
|
|
276a9ea8f8 | ||
|
|
d415f1a0b8 | ||
|
|
6124cf1c04 | ||
|
|
b922346bb6 | ||
|
|
64f1bd78db | ||
|
|
1412a302a1 | ||
|
|
6785427fe8 | ||
|
|
1f0ce7c813 | ||
|
|
f5379795de | ||
|
|
c76db2f84a | ||
|
|
cb6e3ae69b | ||
|
|
7bc2c0e797 | ||
|
|
5a7c514548 | ||
|
|
81cf0011b3 | ||
|
|
94062ffc9f | ||
|
|
0449a25c6a | ||
|
|
ff98ca0c1c | ||
|
|
c9dcbf40e7 | ||
|
|
c8a7887808 | ||
|
|
e4504b2355 | ||
|
|
13cedbe49e | ||
|
|
abb80665af | ||
|
|
681cbcf2dc | ||
|
|
78c9d49359 | ||
|
|
da606dbff0 | ||
|
|
8fda883933 | ||
|
|
c4abe9562e | ||
|
|
5d8e605fe7 | ||
|
|
387f8dffd6 | ||
|
|
b34f52fdce | ||
|
|
a3c12a27eb | ||
|
|
a04cad686f | ||
|
|
61877ed0db | ||
|
|
859877979e | ||
|
|
71c76bd8c8 | ||
|
|
dd52747191 | ||
|
|
fa9434b0de | ||
|
|
770a7a4075 | ||
|
|
f6f0238e0a | ||
|
|
236791f32e | ||
|
|
0181f0a849 | ||
|
|
f334b985e8 | ||
|
|
bfeda40284 | ||
|
|
a4e224ec96 | ||
|
|
d259e68a85 | ||
|
|
926a863d27 |
@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0 # Ensures the full Git history is
|
||||
|
||||
@ -490,7 +490,7 @@ jobs:
|
||||
BRANCH_NAME: ${{ needs.build.outputs.branch_name }}
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Set Up Ruby
|
||||
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
|
||||
|
||||
2
.github/workflows/build-mac-catalyst.yml
vendored
2
.github/workflows/build-mac-catalyst.yml
vendored
@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Checkout project
|
||||
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
4
.github/workflows/build-release-apk.yml
vendored
4
.github/workflows/build-release-apk.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: "0"
|
||||
|
||||
@ -135,7 +135,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
|
||||
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -34,7 +34,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -53,6 +53,7 @@ jobs:
|
||||
BIP47_HD_MNEMONIC: ${{ secrets.BIP47_HD_MNEMONIC}}
|
||||
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
|
||||
HD_MNEMONIC_BIP49: ${{ secrets.HD_MNEMONIC_BIP49 }}
|
||||
HD_MNEMONIC_OLD: ${{ secrets.HD_MNEMONIC_OLD }}
|
||||
HD_MNEMONIC_BIP49_MANY_TX: ${{ secrets.HD_MNEMONIC_BIP49_MANY_TX }}
|
||||
HD_MNEMONIC_BIP84: ${{ secrets.HD_MNEMONIC_BIP84 }}
|
||||
HD_MNEMONIC_BREAD: ${{ secrets.HD_MNEMONIC_BREAD }}
|
||||
@ -64,7 +65,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -83,6 +84,7 @@ jobs:
|
||||
BIP47_HD_MNEMONIC: ${{ secrets.BIP47_HD_MNEMONIC}}
|
||||
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
|
||||
HD_MNEMONIC_BIP49: ${{ secrets.HD_MNEMONIC_BIP49 }}
|
||||
HD_MNEMONIC_OLD: ${{ secrets.HD_MNEMONIC_OLD }}
|
||||
HD_MNEMONIC_BIP49_MANY_TX: ${{ secrets.HD_MNEMONIC_BIP49_MANY_TX }}
|
||||
HD_MNEMONIC_BIP84: ${{ secrets.HD_MNEMONIC_BIP84 }}
|
||||
HD_MNEMONIC_BREAD: ${{ secrets.HD_MNEMONIC_BREAD }}
|
||||
|
||||
4
.github/workflows/e2e-android.yml
vendored
4
.github/workflows/e2e-android.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Free disk space (Ubuntu)
|
||||
run: |
|
||||
@ -86,7 +86,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Free disk space (Ubuntu)
|
||||
run: |
|
||||
|
||||
14
.github/workflows/e2e-ios.yml
vendored
14
.github/workflows/e2e-ios.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
@ -168,7 +168,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
@ -194,9 +194,6 @@ jobs:
|
||||
mkdir -p ios/build/Build/Products/Release-iphonesimulator
|
||||
tar -xzf BlueWallet.app.tar.gz -C ios/build/Build/Products/Release-iphonesimulator
|
||||
|
||||
- name: Disable simulator animations
|
||||
run: defaults write com.apple.iphonesimulator SlowMotionAnimation -bool NO
|
||||
|
||||
# Pre-boot simulator so first detox launchApp lands warm.
|
||||
- name: Pre-boot iOS simulator
|
||||
run: |
|
||||
@ -210,6 +207,13 @@ jobs:
|
||||
xcrun simctl bootstatus "$UDID" -b
|
||||
xcrun simctl launch "$UDID" com.apple.springboard >/dev/null 2>&1 || true
|
||||
|
||||
# Cut animations so detox sync stays steady on slow CI VMs; Reduce Motion makes reanimated skip to final value.
|
||||
- name: Disable simulator animations
|
||||
run: |
|
||||
defaults write com.apple.iphonesimulator SlowMotionAnimation -bool NO
|
||||
xcrun simctl spawn booted defaults write com.apple.Accessibility ReduceMotionEnabled -bool true
|
||||
xcrun simctl spawn booted notifyutil -p com.apple.Accessibility.ReduceMotionStatusDidChange
|
||||
|
||||
- name: Run detox tests
|
||||
timeout-minutes: 360
|
||||
run: |
|
||||
|
||||
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.4'
|
||||
gem 'concurrent-ruby', '< 1.3.8'
|
||||
|
||||
# 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.3)
|
||||
concurrent-ruby (1.3.7)
|
||||
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.4)
|
||||
concurrent-ruby (< 1.3.8)
|
||||
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.3) sha256=4f9cd28965c4dcf83ffd3ea7304f9323277be8525819cb18a3b61edcb56a7c6a
|
||||
concurrent-ruby (1.3.7) sha256=4412caec3a5ea2e5fdc52076724c071a81f2c0593d83b2ac8cbb8ca63b3151b0
|
||||
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
||||
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
|
||||
declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
module.exports = {
|
||||
presets: ['module:@react-native/babel-preset'],
|
||||
// Pin the @babel/runtime version so Metro resolves a single copy instead of
|
||||
// bundling duplicate helpers, which bloats the bundle.
|
||||
// See https://github.com/babel/babel/issues/18050
|
||||
presets: [['module:@react-native/babel-preset', { enableBabelRuntime: '^7.26.0' }]],
|
||||
plugins: ['react-native-worklets/plugin'],
|
||||
};
|
||||
|
||||
@ -2,4 +2,7 @@
|
||||
* Let's keep config vars, constants and definitions here
|
||||
*/
|
||||
|
||||
export const groundControlUri: string = 'https://groundcontrol.bluewallet.io/';
|
||||
export const groundControlUri: string = 'https://groundcontrol.bluewallet.io';
|
||||
|
||||
/** bitcoin-payment-push-service base URL, no trailing slash. Empty = disabled. */
|
||||
export const arkadePaymentPushUri: string = 'https://electrum2.bluewallet.io:444';
|
||||
|
||||
@ -1,23 +1,98 @@
|
||||
import AES from 'crypto-js/aes';
|
||||
import Utf8 from 'crypto-js/enc-utf8';
|
||||
import { cbc } from '@noble/ciphers/aes';
|
||||
import { md5 } from '@noble/hashes/legacy';
|
||||
import { randomBytes } from '@noble/hashes/utils';
|
||||
|
||||
import { areUint8ArraysEqual, base64ToUint8Array, concatUint8Arrays, stringToUint8Array, uint8ArrayToBase64 } from './uint8array-extras';
|
||||
|
||||
/**
|
||||
* OpenSSL EVP_BytesToKey using MD5 with 1 iteration.
|
||||
*
|
||||
* Reproduces the default key+IV derivation used by CryptoJS@4.x's
|
||||
* `AES.encrypt(string, password)` so the on-disk wire format stays
|
||||
* bit-identical after we swap the underlying library.
|
||||
*
|
||||
* D1 = MD5( password || salt )
|
||||
* Di = MD5( D(i-1) || password || salt ) for i ≥ 2
|
||||
* key||iv = D1 || D2 || ... (take first `byteLength` bytes)
|
||||
*
|
||||
* MD5 is intentional: it matches the legacy OpenSSL format. The
|
||||
* cryptographic weakness of MD5 is not relevant here — the function is
|
||||
* only used as a deterministic byte-stretcher; the password's entropy is
|
||||
* what protects the wallet, not MD5.
|
||||
*/
|
||||
export function evpBytesToKeyMd5(password: Uint8Array, salt: Uint8Array, byteLength: number): Uint8Array {
|
||||
if (!Number.isInteger(byteLength) || byteLength < 0) {
|
||||
throw new Error('evpBytesToKeyMd5: byteLength must be a non-negative integer');
|
||||
}
|
||||
const out = new Uint8Array(byteLength);
|
||||
let written = 0;
|
||||
let prev: Uint8Array = new Uint8Array(0);
|
||||
while (written < byteLength) {
|
||||
prev = md5(concatUint8Arrays([prev, password, salt]));
|
||||
const take = Math.min(prev.length, byteLength - written);
|
||||
out.set(prev.subarray(0, take), written);
|
||||
written += take;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// "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 ciphertext = AES.encrypt(data, password);
|
||||
return ciphertext.toString();
|
||||
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]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,44 +26,93 @@ export interface TinySecp256k1InterfaceExtended {
|
||||
signDER(h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array;
|
||||
}
|
||||
|
||||
necc.utils.sha256Sync = (...messages: Uint8Array[]): Uint8Array => {
|
||||
const combinedMessages = messages.reduce((acc, msg) => {
|
||||
const newArray = new Uint8Array(acc.length + msg.length);
|
||||
newArray.set(acc);
|
||||
newArray.set(msg, acc.length);
|
||||
return newArray;
|
||||
}, new Uint8Array(0));
|
||||
return sha256(combinedMessages);
|
||||
};
|
||||
// @noble/hashes types differ slightly from @noble/secp256k1 v3 hash slot typings.
|
||||
necc.hashes.sha256 = sha256 as NonNullable<typeof necc.hashes.sha256>;
|
||||
necc.hashes.hmacSha256 = ((key: Uint8Array, message: Uint8Array) => hmac(sha256, key, message)) as NonNullable<
|
||||
typeof necc.hashes.hmacSha256
|
||||
>;
|
||||
|
||||
necc.utils.hmacSha256Sync = (key: Uint8Array, ...messages: Uint8Array[]): Uint8Array => {
|
||||
const combinedMessages = messages.reduce((acc, msg) => {
|
||||
const newArray = new Uint8Array(acc.length + msg.length);
|
||||
newArray.set(acc);
|
||||
newArray.set(msg, acc.length);
|
||||
return newArray;
|
||||
}, new Uint8Array(0));
|
||||
return hmac(sha256, key, combinedMessages);
|
||||
};
|
||||
|
||||
/* const normal = necc.utils._normalizePrivateKey;
|
||||
// Removed from @noble/secp256k1 v1.7; vendored from noble test vectors.
|
||||
// @see https://github.com/paulmillr/noble-secp256k1/blob/1.7.2/test/index.ts
|
||||
type Hex = string | Uint8Array;
|
||||
type PrivKey = Hex | bigint | number;
|
||||
|
||||
necc.utils.privateAdd = (privateKey: PrivKey, tweak: Hex) => {
|
||||
console.log({ privateKey, tweak });
|
||||
const p = normal(privateKey);
|
||||
const t = normal(tweak);
|
||||
return necc.utils.privateAdd(necc.utils.mod(p + t, necc.CURVE.n));
|
||||
}; */
|
||||
const { mod, secretKeyToScalar, numberToBytesBE, bytesToNumberBE, hexToBytes } = necc.etc;
|
||||
const CURVE_N = necc.Point.CURVE().n;
|
||||
|
||||
function pointFromBytes(p: Uint8Array): necc.Point {
|
||||
if (p.length === 32) {
|
||||
const prefixed = new Uint8Array(33);
|
||||
prefixed[0] = 0x02;
|
||||
prefixed.set(p, 1);
|
||||
return necc.Point.fromBytes(prefixed);
|
||||
}
|
||||
return necc.Point.fromBytes(p);
|
||||
}
|
||||
|
||||
const tweakUtils = {
|
||||
privateAdd: (privateKey: Hex, tweak: Hex): Uint8Array => {
|
||||
const p = secretKeyToScalar(typeof privateKey === 'string' ? hexToBytes(privateKey) : privateKey);
|
||||
const t = secretKeyToScalar(typeof tweak === 'string' ? hexToBytes(tweak) : tweak);
|
||||
return numberToBytesBE(mod(p + t, CURVE_N));
|
||||
},
|
||||
|
||||
privateNegate: (privateKey: Hex): Uint8Array => {
|
||||
const p = secretKeyToScalar(typeof privateKey === 'string' ? hexToBytes(privateKey) : privateKey);
|
||||
return numberToBytesBE(CURVE_N - p);
|
||||
},
|
||||
|
||||
pointAddScalar: (p: Hex, tweak: Hex, isCompressed?: boolean): Uint8Array => {
|
||||
const P = typeof p === 'string' ? necc.Point.fromHex(p) : pointFromBytes(p);
|
||||
const t = secretKeyToScalar(typeof tweak === 'string' ? hexToBytes(tweak) : tweak);
|
||||
const Q = P.add(necc.Point.BASE.multiply(t));
|
||||
if (Q.is0()) throw new Error('Tweaked point at infinity');
|
||||
return Q.toBytes(isCompressed);
|
||||
},
|
||||
|
||||
pointMultiply: (p: Hex, tweak: Hex, isCompressed?: boolean): Uint8Array => {
|
||||
const P = typeof p === 'string' ? necc.Point.fromHex(p) : pointFromBytes(p);
|
||||
const tweakBytes = typeof tweak === 'string' ? hexToBytes(tweak) : tweak;
|
||||
const t = mod(bytesToNumberBE(tweakBytes), CURVE_N);
|
||||
if (t === 0n) throw new Error('Point at infinity');
|
||||
return P.multiply(t).toBytes(isCompressed);
|
||||
},
|
||||
};
|
||||
|
||||
const defaultTrue = (param?: boolean): boolean => param !== false;
|
||||
|
||||
function compactToDER(sig: Uint8Array): Uint8Array {
|
||||
const encodeInt = (bytes: Uint8Array): Uint8Array => {
|
||||
let i = 0;
|
||||
while (i < bytes.length - 1 && bytes[i] === 0) i++;
|
||||
let trimmed = bytes.subarray(i);
|
||||
if (trimmed[0] >= 0x80) {
|
||||
const prefixed = new Uint8Array(trimmed.length + 1);
|
||||
prefixed[0] = 0;
|
||||
prefixed.set(trimmed, 1);
|
||||
trimmed = prefixed;
|
||||
}
|
||||
const encoded = new Uint8Array(2 + trimmed.length);
|
||||
encoded[0] = 0x02;
|
||||
encoded[1] = trimmed.length;
|
||||
encoded.set(trimmed, 2);
|
||||
return encoded;
|
||||
};
|
||||
|
||||
const rDer = encodeInt(sig.subarray(0, 32));
|
||||
const sDer = encodeInt(sig.subarray(32, 64));
|
||||
const seqLen = rDer.length + sDer.length;
|
||||
const der = new Uint8Array(2 + seqLen);
|
||||
der[0] = 0x30;
|
||||
der[1] = seqLen;
|
||||
der.set(rDer, 2);
|
||||
der.set(sDer, 2 + rDer.length);
|
||||
return der;
|
||||
}
|
||||
|
||||
function throwToNull<Type>(fn: () => Type): Type | null {
|
||||
try {
|
||||
return fn();
|
||||
} catch (e) {
|
||||
// console.log(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -71,7 +120,8 @@ function throwToNull<Type>(fn: () => Type): Type | null {
|
||||
function isPoint(p: Uint8Array, xOnly: boolean): boolean {
|
||||
if ((p.length === 32) !== xOnly) return false;
|
||||
try {
|
||||
return !!necc.Point.fromHex(p);
|
||||
pointFromBytes(p);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
@ -79,23 +129,12 @@ function isPoint(p: Uint8Array, xOnly: boolean): boolean {
|
||||
|
||||
const ecc: TinySecp256k1InterfaceExtended & TinySecp256k1Interface & TinySecp256k1InterfaceBIP32 = {
|
||||
isPoint: (p: Uint8Array): boolean => isPoint(p, false),
|
||||
isPrivate: (d: Uint8Array): boolean => {
|
||||
/* if (
|
||||
[
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
|
||||
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142',
|
||||
].includes(d.toString('hex'))
|
||||
) {
|
||||
return false;
|
||||
} */
|
||||
return necc.utils.isValidPrivateKey(d);
|
||||
},
|
||||
isPrivate: (d: Uint8Array): boolean => necc.utils.isValidSecretKey(d),
|
||||
isXOnlyPoint: (p: Uint8Array): boolean => isPoint(p, true),
|
||||
|
||||
xOnlyPointAddTweak: (p: Uint8Array, tweak: Uint8Array): { parity: 0 | 1; xOnlyPubkey: Uint8Array } | null =>
|
||||
throwToNull(() => {
|
||||
const P = necc.utils.pointAddScalar(p, tweak, true);
|
||||
const P = tweakUtils.pointAddScalar(p, tweak, true);
|
||||
const parity = P[0] % 2 === 1 ? 1 : 0;
|
||||
return { parity, xOnlyPubkey: P.slice(1) };
|
||||
}),
|
||||
@ -104,60 +143,56 @@ const ecc: TinySecp256k1InterfaceExtended & TinySecp256k1Interface & TinySecp256
|
||||
throwToNull(() => necc.getPublicKey(sk, defaultTrue(compressed))),
|
||||
|
||||
pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array => {
|
||||
return necc.Point.fromHex(p).toRawBytes(defaultTrue(compressed));
|
||||
return pointFromBytes(p).toBytes(defaultTrue(compressed));
|
||||
},
|
||||
|
||||
pointMultiply: (a: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null =>
|
||||
throwToNull(() => necc.utils.pointMultiply(a, tweak, defaultTrue(compressed))),
|
||||
throwToNull(() => tweakUtils.pointMultiply(a, tweak, defaultTrue(compressed))),
|
||||
|
||||
pointAdd: (a: Uint8Array, b: Uint8Array, compressed?: boolean): Uint8Array | null =>
|
||||
throwToNull(() => {
|
||||
const A = necc.Point.fromHex(a);
|
||||
const B = necc.Point.fromHex(b);
|
||||
return A.add(B).toRawBytes(defaultTrue(compressed));
|
||||
const A = pointFromBytes(a);
|
||||
const B = pointFromBytes(b);
|
||||
return A.add(B).toBytes(defaultTrue(compressed));
|
||||
}),
|
||||
|
||||
pointAddScalar: (p: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null =>
|
||||
throwToNull(() => necc.utils.pointAddScalar(p, tweak, defaultTrue(compressed))),
|
||||
throwToNull(() => tweakUtils.pointAddScalar(p, tweak, defaultTrue(compressed))),
|
||||
|
||||
privateAdd: (d: Uint8Array, tweak: Uint8Array): Uint8Array | null =>
|
||||
throwToNull(() => {
|
||||
// console.log({ d, tweak });
|
||||
if (d.join('') === '00000000000000000000000000000001' && tweak.join('') === '00000000000000000000000000000000') {
|
||||
return new Uint8Array(d); // make test_ecc happy
|
||||
}
|
||||
|
||||
const ret = necc.utils.privateAdd(d, tweak);
|
||||
// console.log(ret);
|
||||
const ret = tweakUtils.privateAdd(d, tweak);
|
||||
if (ret.join('') === '00000000000000000000000000000000') {
|
||||
return null;
|
||||
}
|
||||
return ret;
|
||||
}),
|
||||
|
||||
privateNegate: (d: Uint8Array): Uint8Array => necc.utils.privateNegate(d),
|
||||
privateNegate: (d: Uint8Array): Uint8Array => tweakUtils.privateNegate(d),
|
||||
|
||||
sign: (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array => {
|
||||
return necc.signSync(h, d, { der: false, extraEntropy: e });
|
||||
return necc.sign(h, d, { prehash: false, extraEntropy: e });
|
||||
},
|
||||
|
||||
signDER: (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array => {
|
||||
return necc.signSync(h, d, { der: true, extraEntropy: e });
|
||||
return compactToDER(necc.sign(h, d, { prehash: false, extraEntropy: e }));
|
||||
},
|
||||
|
||||
signSchnorr: (h: Uint8Array, d: Uint8Array, e: Uint8Array = new Uint8Array(32).fill(0x00)): Uint8Array => {
|
||||
return necc.schnorr.signSync(h, d, e);
|
||||
return necc.schnorr.sign(h, d, e);
|
||||
},
|
||||
|
||||
verify: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array, strict?: boolean): boolean => {
|
||||
return necc.verify(signature, h, Q, { strict });
|
||||
return necc.verify(signature, h, Q, { prehash: false, lowS: strict !== false });
|
||||
},
|
||||
|
||||
verifySchnorr: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean => {
|
||||
return necc.schnorr.verifySync(signature, h, Q);
|
||||
return necc.schnorr.verify(signature, h, Q);
|
||||
},
|
||||
};
|
||||
|
||||
export default ecc;
|
||||
|
||||
// module.exports.ecc = ecc;
|
||||
|
||||
@ -8,16 +8,16 @@ import {
|
||||
Notifications,
|
||||
} from 'react-native-notifications';
|
||||
import { checkNotifications, requestNotifications, RESULTS } from 'react-native-permissions';
|
||||
import type { BoltzReverseSwap } from '@arkade-os/boltz-swap';
|
||||
import loc from '../loc';
|
||||
import { groundControlUri } from './constants';
|
||||
import { arkadePaymentPushUri, groundControlUri } from './constants';
|
||||
import { fetch } from '../util/fetch';
|
||||
|
||||
const PUSH_TOKEN = 'PUSH_TOKEN';
|
||||
const GROUNDCONTROL_BASE_URI = 'GROUNDCONTROL_BASE_URI';
|
||||
const NOTIFICATIONS_STORAGE = 'NOTIFICATIONS_STORAGE';
|
||||
const ANDROID_NOTIFICATION_CHANNEL_ID = 'channel_01';
|
||||
export const NOTIFICATIONS_NO_AND_DONT_ASK_FLAG = 'NOTIFICATIONS_NO_AND_DONT_ASK_FLAG';
|
||||
let baseURI = groundControlUri;
|
||||
const baseURI = groundControlUri;
|
||||
let notificationSubscriptions: EmitterSubscription[] = [];
|
||||
let onProcessNotificationsHandler: undefined | (() => void | Promise<void>);
|
||||
const handledNotificationKeys = new Set<string>();
|
||||
@ -252,6 +252,29 @@ export const tryToObtainPermissions = async (): Promise<boolean> => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const enqueueTestPushNotification = async (): Promise<void> => {
|
||||
const pushToken = await getPushToken();
|
||||
if (!pushToken?.token || !pushToken?.os) {
|
||||
throw new Error('No push token available');
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseURI}/enqueue`, {
|
||||
method: 'POST',
|
||||
headers: _getHeaders(),
|
||||
body: JSON.stringify({
|
||||
type: 5,
|
||||
token: pushToken.token,
|
||||
os: pushToken.os,
|
||||
text: 'Test push notification',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Enqueue request failed with status ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Submits onchain bitcoin addresses and ln invoice preimage hashes to GroundControl server, so later we could
|
||||
* be notified if they were paid
|
||||
@ -327,6 +350,44 @@ export const majorTomToGroundControl = async (addresses: string[], hashes: strin
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers an Ark swap with the bitcoin-payment-push-service so the device is
|
||||
* pushed when the invoice gets paid. Fire-and-forget: never throws, gated by
|
||||
* the same opt-out/token rules as majorTomToGroundControl(). The swap's
|
||||
* preimage is always stripped before leaving the device.
|
||||
*/
|
||||
export const registerArkPaymentPush = async (paymentHash: string, label: string, pendingSwap: BoltzReverseSwap): Promise<void> => {
|
||||
if (!arkadePaymentPushUri) return;
|
||||
try {
|
||||
const noAndDontAskFlag = await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG);
|
||||
if (noAndDontAskFlag === 'true') {
|
||||
console.warn('User has opted out of notifications.');
|
||||
return;
|
||||
}
|
||||
|
||||
const pushToken = await getPushToken();
|
||||
if (!pushToken || !pushToken.token || !pushToken.os) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`${arkadePaymentPushUri}/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
topic: paymentHash,
|
||||
label,
|
||||
swap: { ...pendingSwap, preimage: '' },
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`status ${response.status}`);
|
||||
}
|
||||
console.log('[ARK] payment push registration ok');
|
||||
} catch (e: any) {
|
||||
console.log('[ARK] payment push registration failed:', e?.message ?? e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a permissions object:
|
||||
* alert: boolean
|
||||
@ -529,22 +590,6 @@ const configureNotifications = async (onProcessNotifications?: () => void): Prom
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates whether the provided GroundControl URI is valid by pinging it.
|
||||
*
|
||||
* @param uri {string}
|
||||
* @returns {Promise<boolean>} TRUE if valid, FALSE otherwise
|
||||
*/
|
||||
export const isGroundControlUriValid = async (uri: string) => {
|
||||
try {
|
||||
const response = await fetch(`${uri}/ping`, { headers: _getHeaders() });
|
||||
const json = await response.json();
|
||||
return !!json.description;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const isNotificationsCapable = hasGmsSync() || hasHmsSync() || Platform.OS !== 'android';
|
||||
|
||||
export const getPushToken = async (): Promise<TPushToken> => {
|
||||
@ -676,38 +721,6 @@ export const removeAllDeliveredNotifications = () => {
|
||||
Notifications.removeAllDeliveredNotifications();
|
||||
};
|
||||
|
||||
export const getDefaultUri = () => {
|
||||
return groundControlUri;
|
||||
};
|
||||
|
||||
export const saveUri = async (uri: string) => {
|
||||
try {
|
||||
baseURI = uri || groundControlUri;
|
||||
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, baseURI);
|
||||
} catch (error) {
|
||||
console.error('Error saving URI:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getSavedUri = async () => {
|
||||
try {
|
||||
const baseUriStored = await AsyncStorage.getItem(GROUNDCONTROL_BASE_URI);
|
||||
if (baseUriStored) {
|
||||
baseURI = baseUriStored;
|
||||
}
|
||||
return baseUriStored;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
try {
|
||||
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, groundControlUri);
|
||||
} catch (storageError) {
|
||||
console.error('Failed to reset URI:', storageError);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const isNotificationsEnabled = async () => {
|
||||
try {
|
||||
const levels = await getLevels();
|
||||
@ -757,10 +770,6 @@ export const initializeNotifications = async (onProcessNotifications?: () => voi
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUriStored = await AsyncStorage.getItem(GROUNDCONTROL_BASE_URI);
|
||||
baseURI = baseUriStored || groundControlUri;
|
||||
console.log('Base URI set to:', baseURI);
|
||||
|
||||
setApplicationIconBadgeNumber(0);
|
||||
|
||||
// Only check permissions, never request
|
||||
@ -781,7 +790,5 @@ export const initializeNotifications = async (onProcessNotifications?: () => voi
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize notifications:', error);
|
||||
baseURI = groundControlUri;
|
||||
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, groundControlUri).catch(err => console.error('Failed to reset URI:', err));
|
||||
}
|
||||
};
|
||||
|
||||
@ -147,11 +147,10 @@ export class BlueApp {
|
||||
console.warn('error reading', key, error.message);
|
||||
console.warn('fallback to realm');
|
||||
const realmKeyValue = await this.openRealmKeyValue();
|
||||
const obj = realmKeyValue.objectForPrimaryKey('KeyValue', key); // search for a realm object with a primary key
|
||||
const obj = realmKeyValue.objectForPrimaryKey<{ key: string; value: string }>('KeyValue', key);
|
||||
value = obj?.value;
|
||||
realmKeyValue.close();
|
||||
if (value) {
|
||||
// @ts-ignore value.length
|
||||
console.warn('successfully recovered', value.length, 'bytes from realm for key', key);
|
||||
return value;
|
||||
}
|
||||
@ -547,10 +546,11 @@ export class BlueApp {
|
||||
(walletToInflate._txs_by_internal_index[tx.index] as Transaction[]).push(transaction);
|
||||
}
|
||||
} else {
|
||||
if (!Array.isArray(walletToInflate._txs_by_external_index)) walletToInflate._txs_by_external_index = [];
|
||||
walletToInflate._txs_by_external_index = walletToInflate._txs_by_external_index || [];
|
||||
// Legacy single-address wallets - store under index 0
|
||||
walletToInflate._txs_by_external_index = walletToInflate._txs_by_external_index || {};
|
||||
walletToInflate._txs_by_external_index[0] = walletToInflate._txs_by_external_index[0] || [];
|
||||
const transaction = JSON.parse(tx.tx);
|
||||
(walletToInflate._txs_by_external_index as Transaction[]).push(transaction);
|
||||
walletToInflate._txs_by_external_index[0].push(transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -559,32 +559,6 @@ export class BlueApp {
|
||||
const id = wallet.getID();
|
||||
const walletToSave = ('_hdWalletInstance' in wallet && wallet._hdWalletInstance) || wallet;
|
||||
|
||||
if (Array.isArray(walletToSave._txs_by_external_index)) {
|
||||
// if this var is an array that means its a single-address wallet class, and this var is a flat array
|
||||
// with transactions
|
||||
realm.write(() => {
|
||||
// cleanup all existing transactions for the wallet first
|
||||
const walletTransactionsToDelete = realm.objects('WalletTransactions').filtered(`walletid = '${id}'`);
|
||||
realm.delete(walletTransactionsToDelete);
|
||||
|
||||
// @ts-ignore walletToSave._txs_by_external_index is array
|
||||
for (const tx of walletToSave._txs_by_external_index) {
|
||||
realm.create(
|
||||
'WalletTransactions',
|
||||
{
|
||||
walletid: id,
|
||||
tx: JSON.stringify(tx),
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/// ########################################################################################################
|
||||
|
||||
if (walletToSave._txs_by_external_index) {
|
||||
realm.write(() => {
|
||||
// cleanup all existing transactions for the wallet first
|
||||
@ -592,16 +566,14 @@ export class BlueApp {
|
||||
realm.delete(walletTransactionsToDelete);
|
||||
|
||||
// insert new ones:
|
||||
for (const index of Object.keys(walletToSave._txs_by_external_index)) {
|
||||
// @ts-ignore index is number
|
||||
const txs = walletToSave._txs_by_external_index[index];
|
||||
for (const [indexStr, txs] of Object.entries(walletToSave._txs_by_external_index)) {
|
||||
for (const tx of txs) {
|
||||
realm.create(
|
||||
'WalletTransactions',
|
||||
{
|
||||
walletid: id,
|
||||
internal: false,
|
||||
index: parseInt(index, 10),
|
||||
index: parseInt(indexStr, 10),
|
||||
tx: JSON.stringify(tx),
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
@ -609,16 +581,14 @@ export class BlueApp {
|
||||
}
|
||||
}
|
||||
|
||||
for (const index of Object.keys(walletToSave._txs_by_internal_index)) {
|
||||
// @ts-ignore index is number
|
||||
const txs = walletToSave._txs_by_internal_index[index];
|
||||
for (const [indexStr, txs] of Object.entries(walletToSave._txs_by_internal_index)) {
|
||||
for (const tx of txs) {
|
||||
realm.create(
|
||||
'WalletTransactions',
|
||||
{
|
||||
walletid: id,
|
||||
internal: true,
|
||||
index: parseInt(index, 10),
|
||||
index: parseInt(indexStr, 10),
|
||||
tx: JSON.stringify(tx),
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
|
||||
@ -390,7 +390,7 @@ export class HDSegwitBech32Transaction {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore stfu
|
||||
return { tx, inputs, outputs, fee };
|
||||
// Non-null assertions are safe here because the while loop always runs at least once (add starts at 0)
|
||||
return { tx: tx!, inputs: inputs!, outputs: outputs!, fee: fee! };
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 CryptoJS from 'crypto-js';
|
||||
import { cbc } from '@noble/ciphers/aes';
|
||||
import ecc from '../blue_modules/noble_ecc';
|
||||
import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api
|
||||
import { fetch } from '../util/fetch';
|
||||
@ -321,13 +321,24 @@ export default class Lnurl {
|
||||
}
|
||||
|
||||
static decipherAES(ciphertextBase64: string, preimageHex: string, ivBase64: string): string {
|
||||
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);
|
||||
// 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 '';
|
||||
}
|
||||
}
|
||||
|
||||
getCommentAllowed(): number | false {
|
||||
|
||||
@ -106,23 +106,31 @@ export class MultisigCosigner {
|
||||
this._valid = false;
|
||||
}
|
||||
|
||||
// is it coldcard json?
|
||||
// is it coldcard / unchained json?
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (json.p2sh && json.p2sh_deriv && json.xfp) {
|
||||
const cc = new MultisigCosigner(MultisigCosigner.exportToJson(json.xfp, json.p2sh, json.p2sh_deriv));
|
||||
|
||||
// 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));
|
||||
this._valid = true;
|
||||
this._cosigners.push(cc);
|
||||
}
|
||||
|
||||
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));
|
||||
if (xpub && path && json.xfp) {
|
||||
const cc = new MultisigCosigner(MultisigCosigner.exportToJson(json.xfp, xpub, path));
|
||||
this._valid = true;
|
||||
this._cosigners.push(cc);
|
||||
}
|
||||
|
||||
if (json.p2wsh && json.p2wsh_deriv && json.xfp) {
|
||||
const cc = new MultisigCosigner(MultisigCosigner.exportToJson(json.xfp, json.p2wsh, json.p2wsh_deriv));
|
||||
if (json.p2wsh && p2wsh_deriv && json.xfp) {
|
||||
const cc = new MultisigCosigner(MultisigCosigner.exportToJson(json.xfp, json.p2wsh, p2wsh_deriv));
|
||||
this._valid = true;
|
||||
this._cosigners.push(cc);
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
* @return {Promise.<Uint8Array>} The random bytes
|
||||
*/
|
||||
export async function randomBytes(size: number): Promise<Uint8Array> {
|
||||
const g: any = globalThis as any;
|
||||
const g = globalThis as any;
|
||||
const rnCrypto = g && g.crypto;
|
||||
if (!rnCrypto || typeof rnCrypto.getRandomValues !== 'function') {
|
||||
throw new Error('crypto.getRandomValues is not available');
|
||||
|
||||
@ -45,9 +45,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
_balances_by_external_index: Record<number, BalanceByIndex>;
|
||||
_balances_by_internal_index: Record<number, BalanceByIndex>;
|
||||
|
||||
// @ts-ignore
|
||||
_txs_by_external_index: Record<number, Transaction[]>;
|
||||
// @ts-ignore
|
||||
_txs_by_internal_index: Record<number, Transaction[]>;
|
||||
|
||||
_utxo: any[];
|
||||
@ -204,70 +202,37 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
return child.toWIF();
|
||||
}
|
||||
|
||||
_getNodeAddressByIndex(node: number, index: number): string {
|
||||
index = index * 1; // cast to int
|
||||
_getNodeByIndex(node: 0 | 1, index: number): BIP32Interface {
|
||||
const cachedNode = node === 0 ? this._node0 : this._node1;
|
||||
if (cachedNode) {
|
||||
return cachedNode.derive(index);
|
||||
}
|
||||
|
||||
const xpub = this._zpubToXpub(this.getXpub());
|
||||
const hdNode = bip32.fromBase58(xpub).derive(node);
|
||||
|
||||
if (node === 0) {
|
||||
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
|
||||
}
|
||||
|
||||
if (node === 1) {
|
||||
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
|
||||
}
|
||||
|
||||
if (node === 0 && !this._node0) {
|
||||
const xpub = this._zpubToXpub(this.getXpub());
|
||||
const hdNode = bip32.fromBase58(xpub);
|
||||
this._node0 = hdNode.derive(node);
|
||||
}
|
||||
|
||||
if (node === 1 && !this._node1) {
|
||||
const xpub = this._zpubToXpub(this.getXpub());
|
||||
const hdNode = bip32.fromBase58(xpub);
|
||||
this._node1 = hdNode.derive(node);
|
||||
}
|
||||
|
||||
let address: string;
|
||||
if (node === 0) {
|
||||
// @ts-ignore
|
||||
address = this._hdNodeToAddress(this._node0.derive(index));
|
||||
this._node0 = hdNode;
|
||||
} else {
|
||||
// tbh the only possible else is node === 1
|
||||
// @ts-ignore
|
||||
address = this._hdNodeToAddress(this._node1.derive(index));
|
||||
this._node1 = hdNode;
|
||||
}
|
||||
|
||||
if (node === 0) {
|
||||
return (this.external_addresses_cache[index] = address);
|
||||
} else {
|
||||
// tbh the only possible else option is node === 1
|
||||
return (this.internal_addresses_cache[index] = address);
|
||||
}
|
||||
return hdNode.derive(index);
|
||||
}
|
||||
|
||||
_getNodePubkeyByIndex(node: number, index: number) {
|
||||
index = index * 1; // cast to int
|
||||
_getNodeAddressByIndex(node: 0 | 1, index: number): string {
|
||||
const cache = node === 0 ? this.external_addresses_cache : this.internal_addresses_cache;
|
||||
|
||||
if (node === 0 && !this._node0) {
|
||||
const xpub = this._zpubToXpub(this.getXpub());
|
||||
const hdNode = bip32.fromBase58(xpub);
|
||||
this._node0 = hdNode.derive(node);
|
||||
}
|
||||
if (cache[index]) return cache[index]; // cache hit
|
||||
|
||||
if (node === 1 && !this._node1) {
|
||||
const xpub = this._zpubToXpub(this.getXpub());
|
||||
const hdNode = bip32.fromBase58(xpub);
|
||||
this._node1 = hdNode.derive(node);
|
||||
}
|
||||
const hdNode = this._getNodeByIndex(node, index);
|
||||
const address = this._hdNodeToAddress(hdNode);
|
||||
|
||||
if (node === 0 && this._node0) {
|
||||
return this._node0.derive(index).publicKey;
|
||||
}
|
||||
return (cache[index] = address);
|
||||
}
|
||||
|
||||
if (node === 1 && this._node1) {
|
||||
return this._node1.derive(index).publicKey;
|
||||
}
|
||||
|
||||
throw new Error('Internal error: this._node0 or this._node1 is undefined');
|
||||
_getNodePubkeyByIndex(node: 0 | 1, index: number) {
|
||||
return this._getNodeByIndex(node, index).publicKey;
|
||||
}
|
||||
|
||||
_getExternalAddressByIndex(index: number): string {
|
||||
@ -424,137 +389,95 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
// now, we need to put transactions in all relevant `cells` of internal hashmaps:
|
||||
// this._txs_by_internal_index, this._txs_by_external_index & this._txs_by_payment_code_index
|
||||
|
||||
// address -> index lookup maps; the single pass over transactions below uses them
|
||||
// to find which cells a transaction belongs to
|
||||
const externalIndexByAddress = new Map<string, number>();
|
||||
for (let c = 0; c < next_free_address_index + this.gap_limit; c++) {
|
||||
for (const tx of Object.values(txdatas)) {
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
|
||||
// this TX is related to our address
|
||||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
|
||||
const { vin: txVin, vout: txVout, ...txRest } = tx;
|
||||
const clonedTx = {
|
||||
...txRest,
|
||||
inputs: txVin.slice(0),
|
||||
outputs: txVout.slice(0),
|
||||
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
|
||||
};
|
||||
|
||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||
let replaced = false;
|
||||
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) {
|
||||
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) {
|
||||
replaced = true;
|
||||
this._txs_by_external_index[c][cc] = clonedTx;
|
||||
}
|
||||
}
|
||||
if (!replaced) this._txs_by_external_index[c].push(clonedTx);
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
|
||||
// this TX is related to our address
|
||||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
|
||||
const { vin: txVin, vout: txVout, ...txRest } = tx;
|
||||
const clonedTx = {
|
||||
...txRest,
|
||||
inputs: txVin.slice(0),
|
||||
outputs: txVout.slice(0),
|
||||
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
|
||||
};
|
||||
|
||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||
let replaced = false;
|
||||
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) {
|
||||
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) {
|
||||
replaced = true;
|
||||
this._txs_by_external_index[c][cc] = clonedTx;
|
||||
}
|
||||
}
|
||||
if (!replaced) this._txs_by_external_index[c].push(clonedTx);
|
||||
}
|
||||
}
|
||||
}
|
||||
externalIndexByAddress.set(this._getExternalAddressByIndex(c), c);
|
||||
}
|
||||
|
||||
const internalIndexByAddress = new Map<string, number>();
|
||||
for (let c = 0; c < next_free_change_address_index + this.gap_limit; c++) {
|
||||
for (const tx of Object.values(txdatas)) {
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
|
||||
// this TX is related to our address
|
||||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
|
||||
const { vin: txVin, vout: txVout, ...txRest } = tx;
|
||||
const clonedTx = {
|
||||
...txRest,
|
||||
inputs: txVin.slice(0),
|
||||
outputs: txVout.slice(0),
|
||||
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
|
||||
};
|
||||
|
||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||
let replaced = false;
|
||||
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) {
|
||||
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) {
|
||||
replaced = true;
|
||||
this._txs_by_internal_index[c][cc] = clonedTx;
|
||||
}
|
||||
}
|
||||
if (!replaced) this._txs_by_internal_index[c].push(clonedTx);
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
|
||||
// this TX is related to our address
|
||||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
|
||||
const { vin: txVin, vout: txVout, ...txRest } = tx;
|
||||
const clonedTx = {
|
||||
...txRest,
|
||||
inputs: txVin.slice(0),
|
||||
outputs: txVout.slice(0),
|
||||
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
|
||||
};
|
||||
|
||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||
let replaced = false;
|
||||
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) {
|
||||
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) {
|
||||
replaced = true;
|
||||
this._txs_by_internal_index[c][cc] = clonedTx;
|
||||
}
|
||||
}
|
||||
if (!replaced) this._txs_by_internal_index[c].push(clonedTx);
|
||||
}
|
||||
}
|
||||
}
|
||||
internalIndexByAddress.set(this._getInternalAddressByIndex(c), c);
|
||||
}
|
||||
|
||||
const paymentCodeIndexByAddress = new Map<string, { pc: string; c: number }>();
|
||||
for (const pc of this._receive_payment_codes) {
|
||||
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
|
||||
for (const tx of Object.values(txdatas)) {
|
||||
// since we are iterating PCs who can pay us, we can completely ignore `tx.vin` and only iterate `tx.vout`
|
||||
for (const vout of tx.vout) {
|
||||
if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.indexOf(this._getBIP47AddressReceive(pc, c)) !== -1) {
|
||||
// this TX is related to our address
|
||||
this._txs_by_payment_code_index[pc] = this._txs_by_payment_code_index[pc] || {};
|
||||
this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c] || [];
|
||||
const { vin: txVin, vout: txVout, ...txRest } = tx;
|
||||
const clonedTx = {
|
||||
...txRest,
|
||||
inputs: txVin.slice(0),
|
||||
outputs: txVout.slice(0),
|
||||
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
|
||||
};
|
||||
paymentCodeIndexByAddress.set(this._getBIP47AddressReceive(pc, c), { pc, c });
|
||||
}
|
||||
}
|
||||
|
||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||
let replaced = false;
|
||||
for (let cc = 0; cc < this._txs_by_payment_code_index[pc][c].length; cc++) {
|
||||
if (this._txs_by_payment_code_index[pc][c][cc].txid === clonedTx.txid) {
|
||||
replaced = true;
|
||||
this._txs_by_payment_code_index[pc][c][cc] = clonedTx;
|
||||
}
|
||||
}
|
||||
if (!replaced) this._txs_by_payment_code_index[pc][c].push(clonedTx);
|
||||
}
|
||||
}
|
||||
// per-cell txid -> position lookup, used to replace-or-push a transaction into a cell in constant time
|
||||
const cellPositionsByTxid = new Map<Transaction[], Map<string, number>>();
|
||||
const getCellPositions = (cell: Transaction[]): Map<string, number> => {
|
||||
let positions = cellPositionsByTxid.get(cell);
|
||||
if (!positions) {
|
||||
positions = new Map();
|
||||
for (let cc = 0; cc < cell.length; cc++) positions.set(cell[cc].txid, cc);
|
||||
cellPositionsByTxid.set(cell, positions);
|
||||
}
|
||||
return positions;
|
||||
};
|
||||
|
||||
for (const tx of Object.values(txdatas)) {
|
||||
// collecting which of our address `cells` this transaction touches:
|
||||
const externalCells = new Set<number>();
|
||||
const internalCells = new Set<number>();
|
||||
const paymentCodeCells = new Map<string, { pc: string; c: number }>();
|
||||
|
||||
const matchAddress = (address: string, isVout: boolean) => {
|
||||
const externalIndex = externalIndexByAddress.get(address);
|
||||
if (externalIndex !== undefined) externalCells.add(externalIndex);
|
||||
const internalIndex = internalIndexByAddress.get(address);
|
||||
if (internalIndex !== undefined) internalCells.add(internalIndex);
|
||||
if (isVout) {
|
||||
// since we are iterating PCs who can pay us, we can completely ignore `tx.vin` and only check `tx.vout`
|
||||
const paymentCodeIndex = paymentCodeIndexByAddress.get(address);
|
||||
if (paymentCodeIndex) paymentCodeCells.set(address, paymentCodeIndex);
|
||||
}
|
||||
};
|
||||
|
||||
for (const vin of tx.vin) {
|
||||
for (const address of vin.addresses ?? []) matchAddress(address, false);
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
for (const address of vout.scriptPubKey.addresses ?? []) matchAddress(address, true);
|
||||
}
|
||||
|
||||
if (externalCells.size === 0 && internalCells.size === 0 && paymentCodeCells.size === 0) continue;
|
||||
|
||||
// this TX is related to our address(es)
|
||||
const upsertClone = (cell: Transaction[]) => {
|
||||
const { vin: txVin, vout: txVout, ...txRest } = tx;
|
||||
const clonedTx = {
|
||||
...txRest,
|
||||
inputs: txVin.slice(0),
|
||||
outputs: txVout.slice(0),
|
||||
timestamp: tx.blocktime || tx.time || Math.floor(+new Date() / 1000) - 30 /* unconfirmed */,
|
||||
};
|
||||
|
||||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
||||
const positions = getCellPositions(cell);
|
||||
const existingPosition = positions.get(clonedTx.txid);
|
||||
if (existingPosition !== undefined) {
|
||||
cell[existingPosition] = clonedTx;
|
||||
} else {
|
||||
positions.set(clonedTx.txid, cell.length);
|
||||
cell.push(clonedTx);
|
||||
}
|
||||
};
|
||||
|
||||
for (const c of externalCells) {
|
||||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
|
||||
upsertClone(this._txs_by_external_index[c]);
|
||||
}
|
||||
for (const c of internalCells) {
|
||||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
|
||||
upsertClone(this._txs_by_internal_index[c]);
|
||||
}
|
||||
for (const { pc, c } of paymentCodeCells.values()) {
|
||||
this._txs_by_payment_code_index[pc] = this._txs_by_payment_code_index[pc] || {};
|
||||
this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c] || [];
|
||||
upsertClone(this._txs_by_payment_code_index[pc][c]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -652,8 +575,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
let lastHistoriesWithUsedAddresses = null;
|
||||
for (let c = 0; c < Math.round(index / this.gap_limit); c++) {
|
||||
const histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c));
|
||||
// @ts-ignore
|
||||
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
|
||||
if (AbstractHDElectrumWallet._getTransactionsFromHistories(histories).length > 0) {
|
||||
// in this particular chunk we have used addresses
|
||||
lastChunkWithUsedAddressesNum = c;
|
||||
lastHistoriesWithUsedAddresses = histories;
|
||||
@ -695,8 +617,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
let lastHistoriesWithUsedAddresses = null;
|
||||
for (let c = 0; c < Math.round(index / this.gap_limit); c++) {
|
||||
const histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c));
|
||||
// @ts-ignore
|
||||
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
|
||||
if (AbstractHDElectrumWallet._getTransactionsFromHistories(histories).length > 0) {
|
||||
// in this particular chunk we have used addresses
|
||||
lastChunkWithUsedAddressesNum = c;
|
||||
lastHistoriesWithUsedAddresses = histories;
|
||||
@ -738,8 +659,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
let lastHistoriesWithUsedAddresses = null;
|
||||
for (let c = 0; c < Math.round(index / this.gap_limit); c++) {
|
||||
const histories = await BlueElectrum.multiGetHistoryByAddress(generateChunkAddresses(c));
|
||||
// @ts-ignore
|
||||
if (this.constructor._getTransactionsFromHistories(histories).length > 0) {
|
||||
if (AbstractHDElectrumWallet._getTransactionsFromHistories(histories).length > 0) {
|
||||
// in this particular chunk we have used addresses
|
||||
lastChunkWithUsedAddressesNum = c;
|
||||
lastHistoriesWithUsedAddresses = histories;
|
||||
|
||||
@ -315,7 +315,7 @@ export class AbstractHDWallet extends LegacyWallet {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
_getNodePubkeyByIndex(node: number, index: number): Uint8Array | undefined {
|
||||
_getNodePubkeyByIndex(node: 0 | 1, index: number): Uint8Array | undefined {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
|
||||
@ -27,8 +27,8 @@ export class LegacyWallet extends AbstractWallet {
|
||||
// @ts-ignore: override
|
||||
public readonly typeReadable: string;
|
||||
|
||||
_txs_by_external_index: Transaction[] = [];
|
||||
_txs_by_internal_index: Transaction[] = [];
|
||||
_txs_by_external_index: Record<number, Transaction[]> = {};
|
||||
_txs_by_internal_index: Record<number, Transaction[]> = {};
|
||||
|
||||
constructor(typeReadable?: string) {
|
||||
super();
|
||||
@ -344,14 +344,14 @@ export class LegacyWallet extends AbstractWallet {
|
||||
}
|
||||
}
|
||||
|
||||
this._txs_by_external_index = _txsByExternalIndex;
|
||||
this._txs_by_external_index = { 0: _txsByExternalIndex };
|
||||
this._lastTxFetch = +new Date();
|
||||
}
|
||||
|
||||
getTransactions(): Transaction[] {
|
||||
// a hacky code reuse from electrum HD wallet:
|
||||
this._txs_by_external_index = this._txs_by_external_index || [];
|
||||
this._txs_by_internal_index = [];
|
||||
this._txs_by_external_index = this._txs_by_external_index || {};
|
||||
this._txs_by_internal_index = {};
|
||||
|
||||
const { HDSegwitBech32Wallet } = require('./hd-segwit-bech32-wallet') as {
|
||||
HDSegwitBech32Wallet: typeof HDSegwitBech32WalletT;
|
||||
|
||||
@ -29,6 +29,7 @@ import assert from 'assert';
|
||||
import ecc from '../../blue_modules/noble_ecc.ts';
|
||||
import { Measure } from '../measure.ts';
|
||||
import { deleteArkadeRealm, getArkadeRealm } from '../../blue_modules/arkade-adapters/realm/realmInstance';
|
||||
import { registerArkPaymentPush } from '../../blue_modules/notifications';
|
||||
const { bech32m } = require('bech32');
|
||||
|
||||
const bip32 = BIP32Factory(ecc);
|
||||
@ -657,6 +658,12 @@ 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);
|
||||
@ -710,6 +717,8 @@ export class LightningArkWallet extends LightningCustodianWallet {
|
||||
console.log('Pending swap', result.pendingSwap);
|
||||
console.log('Preimage', result.preimage);
|
||||
|
||||
registerArkPaymentPush(result.paymentHash, memo, result.pendingSwap); // fire-and-forget, never throws
|
||||
|
||||
return result.invoice;
|
||||
}
|
||||
|
||||
|
||||
@ -52,7 +52,14 @@ const useFloatButtonAnimation = (initialHeight: number) => {
|
||||
};
|
||||
};
|
||||
|
||||
const useFloatButtonLayout = (width: number, sizeClass: SizeClass) => {
|
||||
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 lastVerticalDecision = useRef(false);
|
||||
|
||||
const shouldUseVerticalLayout = useCallback(
|
||||
@ -152,15 +159,19 @@ const useFloatButtonLayout = (width: number, sizeClass: SizeClass) => {
|
||||
[width, sizeClass, shouldUseVerticalLayout],
|
||||
);
|
||||
|
||||
const calculateContainerHeight = useCallback((childrenCount: number, isVerticalLayout: boolean) => {
|
||||
if (!isVerticalLayout) return { height: '8%', minHeight: LAYOUT.BUTTON_HEIGHT };
|
||||
const calculateContainerHeight = useCallback(
|
||||
(childrenCount: number, isVerticalLayout: boolean) => {
|
||||
const buttonHeight = getScaledButtonHeight(fontScale);
|
||||
if (!isVerticalLayout) return { height: '8%', minHeight: buttonHeight };
|
||||
|
||||
const totalButtonsHeight = childrenCount * LAYOUT.BUTTON_HEIGHT;
|
||||
const totalMarginsHeight = (childrenCount - 1) * LAYOUT.BUTTON_MARGIN;
|
||||
const calculatedHeight = totalButtonsHeight + totalMarginsHeight;
|
||||
const totalButtonsHeight = childrenCount * buttonHeight;
|
||||
const totalMarginsHeight = (childrenCount - 1) * LAYOUT.BUTTON_MARGIN;
|
||||
const calculatedHeight = totalButtonsHeight + totalMarginsHeight;
|
||||
|
||||
return { height: calculatedHeight };
|
||||
}, []);
|
||||
return { height: calculatedHeight };
|
||||
},
|
||||
[fontScale],
|
||||
);
|
||||
|
||||
const calculateButtonFontSize = useMemo(() => {
|
||||
const divisor = sizeClass === SizeClass.Large ? 22 : sizeClass === SizeClass.Regular ? 24 : 28;
|
||||
@ -267,6 +278,7 @@ interface FButtonProps {
|
||||
isVertical?: boolean;
|
||||
borderRadius?: number;
|
||||
fontSize?: number;
|
||||
buttonHeight?: number;
|
||||
disabled?: boolean;
|
||||
testID?: string;
|
||||
onPress: () => void;
|
||||
@ -277,13 +289,14 @@ 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 }: ButtonContentProps) => {
|
||||
const ButtonContent = ({ icon, text, textStyle, buttonHeight }: ButtonContentProps) => {
|
||||
const computedStyle = StyleSheet.flatten(textStyle);
|
||||
const fontSize = computedStyle.fontSize || LAYOUT.MAX_BUTTON_FONT_SIZE;
|
||||
const iconSize = getScaledIconSize(Number(fontSize));
|
||||
@ -307,9 +320,14 @@ const ButtonContent = ({ icon, text, textStyle }: ButtonContentProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={buttonContentStaticStyles.contentContainer}>
|
||||
<View style={[buttonContentStaticStyles.contentContainer, { minHeight: buttonHeight }]}>
|
||||
<View style={buttonStyles.iconContainer}>{scaledIcon}</View>
|
||||
<Text numberOfLines={1} adjustsFontSizeToFit style={[textStyle, buttonStyles.centeredText, { lineHeight: fontSize * 1.2 }]}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.8}
|
||||
style={[textStyle, buttonStyles.centeredText, { lineHeight: fontSize * 1.2 }]}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
@ -325,6 +343,7 @@ export const FButton = ({
|
||||
isVertical,
|
||||
borderRadius = LAYOUT.PILL_BORDER_RADIUS,
|
||||
fontSize = LAYOUT.MAX_BUTTON_FONT_SIZE,
|
||||
buttonHeight = LAYOUT.BUTTON_HEIGHT,
|
||||
testID,
|
||||
...props
|
||||
}: FButtonProps) => {
|
||||
@ -347,6 +366,8 @@ export const FButton = ({
|
||||
return {
|
||||
root: {
|
||||
...baseStyles,
|
||||
height: buttonHeight,
|
||||
minHeight: buttonHeight,
|
||||
backgroundColor: colors.buttonBackgroundColor,
|
||||
},
|
||||
text: {
|
||||
@ -360,7 +381,7 @@ export const FButton = ({
|
||||
marginBottom: buttonContentStaticStyles.marginBottom,
|
||||
textBase: buttonContentStaticStyles.textBase,
|
||||
};
|
||||
}, [colors, fontSize]);
|
||||
}, [colors, fontSize, buttonHeight]);
|
||||
|
||||
const style: Record<string, any> = {};
|
||||
const additionalStyles = !last ? (isVertical ? customButtonStyles.marginBottom : customButtonStyles.marginRight) : {};
|
||||
@ -397,7 +418,7 @@ export const FButton = ({
|
||||
style={[buttonStyles.root, customButtonStyles.root, style, { borderRadius }]}
|
||||
{...props}
|
||||
>
|
||||
<ButtonContent icon={icon} text={text} textStyle={textStyle} />
|
||||
<ButtonContent icon={icon} text={text} textStyle={textStyle} buttonHeight={buttonHeight} />
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
@ -405,8 +426,9 @@ export const FButton = ({
|
||||
|
||||
export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { height, width } = useWindowDimensions();
|
||||
const { height, width, fontScale } = useWindowDimensions();
|
||||
const { sizeClass } = useSizeClass();
|
||||
const scaledButtonHeight = getScaledButtonHeight(fontScale);
|
||||
|
||||
const childrenCount = React.Children.toArray(props.children).filter(Boolean).length;
|
||||
|
||||
@ -419,6 +441,7 @@ 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,
|
||||
@ -508,7 +531,7 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
|
||||
|
||||
useEffect(() => {
|
||||
debouncedCalculateLayout();
|
||||
}, [debouncedCalculateLayout, width, height, childrenCount, sizeClass]);
|
||||
}, [debouncedCalculateLayout, width, height, childrenCount, sizeClass, fontScale]);
|
||||
|
||||
const onLayout = (event: { nativeEvent: { layout: { width: number } } }) => {
|
||||
const { width: currentLayoutWidth } = event.nativeEvent.layout;
|
||||
@ -545,6 +568,7 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
|
||||
isVertical,
|
||||
borderRadius: buttonBorderRadius,
|
||||
fontSize: buttonFontSize,
|
||||
buttonHeight: scaledButtonHeight,
|
||||
});
|
||||
};
|
||||
|
||||
@ -561,10 +585,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 : null,
|
||||
isVertical ? containerHeight : { minHeight: scaledButtonHeight },
|
||||
{ transform: [{ translateY: slideAnimation }] },
|
||||
],
|
||||
[props.inline, bottomInsets, effectiveNewWidth, isVertical, containerHeight, slideAnimation],
|
||||
[props.inline, bottomInsets, effectiveNewWidth, isVertical, containerHeight, slideAnimation, scaledButtonHeight],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Pressable, StyleProp, StyleSheet, Switch, SwitchProps, Text, TextStyle, View, ViewStyle } from 'react-native';
|
||||
import { Pressable, StyleProp, StyleSheet, Switch, SwitchProps, Text, TextStyle, useWindowDimensions, 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>;
|
||||
@ -21,6 +24,7 @@ interface ListItemProps {
|
||||
subtitleNumberOfLines?: number;
|
||||
rightTitle?: string;
|
||||
rightTitleStyle?: StyleProp<TextStyle>;
|
||||
rightTitleSelectable?: boolean;
|
||||
rightSubtitle?: string | React.ReactNode;
|
||||
rightSubtitleStyle?: StyleProp<TextStyle>;
|
||||
chevron?: boolean;
|
||||
@ -45,6 +49,7 @@ const ListItem: React.FC<ListItemProps> = React.memo(
|
||||
subtitleNumberOfLines,
|
||||
rightTitle,
|
||||
rightTitleStyle,
|
||||
rightTitleSelectable,
|
||||
rightSubtitle,
|
||||
rightSubtitleStyle,
|
||||
chevron,
|
||||
@ -53,12 +58,20 @@ 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: {
|
||||
@ -70,7 +83,7 @@ const ListItem: React.FC<ListItemProps> = React.memo(
|
||||
color: colors.alternativeTextColor,
|
||||
fontWeight: '400',
|
||||
paddingVertical: switchProps ? 8 : 0,
|
||||
lineHeight: 20,
|
||||
lineHeight: Math.round(20 * fontScale),
|
||||
fontSize: 14,
|
||||
marginTop: 2,
|
||||
},
|
||||
@ -91,7 +104,7 @@ const ListItem: React.FC<ListItemProps> = React.memo(
|
||||
const enableFeedback = !noFeedback && !!onPress && !disabled;
|
||||
|
||||
const renderContent = () => (
|
||||
<View style={styles.contentRow}>
|
||||
<View style={[styles.contentRow, contentRowStyle]}>
|
||||
{leftAvatar && (
|
||||
<View style={styles.leftAvatarContainer}>
|
||||
{leftAvatar}
|
||||
@ -112,7 +125,14 @@ const ListItem: React.FC<ListItemProps> = React.memo(
|
||||
{rightTitle || rightSubtitle ? (
|
||||
<View style={styles.rightColumn}>
|
||||
{rightTitle ? (
|
||||
<Text style={rightTitleStyle} numberOfLines={1} accessibilityRole="text">
|
||||
<Text
|
||||
style={rightTitleStyle}
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.75}
|
||||
accessibilityRole="text"
|
||||
selectable={rightTitleSelectable}
|
||||
>
|
||||
{rightTitle}
|
||||
</Text>
|
||||
) : null}
|
||||
@ -190,16 +210,20 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
leftAvatarContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
rightColumn: {
|
||||
marginStart: 8,
|
||||
minWidth: 0,
|
||||
flexShrink: 0,
|
||||
alignItems: 'flex-end',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
rightMemoWrapper: {
|
||||
flexShrink: 1,
|
||||
|
||||
@ -216,24 +216,11 @@ const QRCode: React.FC<QRCodeProps> = ({
|
||||
const gradFill = `url(#${GRADIENT_ID})`;
|
||||
|
||||
const finderShapes: React.ReactElement[] = [];
|
||||
const outerR = 2 * cell;
|
||||
const holeR = 1.25 * cell;
|
||||
const dotR = 0.9 * cell;
|
||||
finderOrigins.forEach(([fr, fc], i) => {
|
||||
const x = (fc + 1) * cell;
|
||||
const y = (fr + 1) * cell;
|
||||
finderShapes.push(
|
||||
<Rect
|
||||
key={`finder-frame-${i}`}
|
||||
testID="qr-finder-frame"
|
||||
x={x}
|
||||
y={y}
|
||||
width={7 * cell}
|
||||
height={7 * cell}
|
||||
rx={outerR}
|
||||
ry={outerR}
|
||||
fill={gradFill}
|
||||
/>,
|
||||
<Rect key={`finder-frame-${i}`} testID="qr-finder-frame" x={x} y={y} width={7 * cell} height={7 * cell} fill={gradFill} />,
|
||||
<Rect
|
||||
key={`finder-hole-${i}`}
|
||||
testID="qr-finder-hole"
|
||||
@ -241,8 +228,6 @@ const QRCode: React.FC<QRCodeProps> = ({
|
||||
y={y + cell}
|
||||
width={5 * cell}
|
||||
height={5 * cell}
|
||||
rx={holeR}
|
||||
ry={holeR}
|
||||
fill={BACKGROUND}
|
||||
/>,
|
||||
<Rect
|
||||
@ -252,8 +237,6 @@ const QRCode: React.FC<QRCodeProps> = ({
|
||||
y={y + 2 * cell}
|
||||
width={3 * cell}
|
||||
height={3 * cell}
|
||||
rx={dotR}
|
||||
ry={dotR}
|
||||
fill={gradFill}
|
||||
/>,
|
||||
);
|
||||
@ -277,16 +260,7 @@ const QRCode: React.FC<QRCodeProps> = ({
|
||||
{finderShapes}
|
||||
{isLogoRendered && logoCells > 0 && (
|
||||
<>
|
||||
<Rect
|
||||
testID="qr-logo-backdrop"
|
||||
x={backdropX}
|
||||
y={backdropY}
|
||||
width={backdropSize}
|
||||
height={backdropSize}
|
||||
rx={cell * 0.5}
|
||||
ry={cell * 0.5}
|
||||
fill={LOGO_BACKGROUND}
|
||||
/>
|
||||
<Rect testID="qr-logo-backdrop" x={backdropX} y={backdropY} width={backdropSize} height={backdropSize} fill={LOGO_BACKGROUND} />
|
||||
<SvgImage
|
||||
testID="qr-logo-image"
|
||||
href={require('../img/qr-code.png')}
|
||||
|
||||
@ -67,26 +67,25 @@ const ReplaceFeeSuggestions: React.FC<ReplaceFeeSuggestionsProps> = ({ onFeeSele
|
||||
}, []);
|
||||
|
||||
const handleFeeSelection = (feeType: NetworkTransactionFeeType) => {
|
||||
if (feeType !== NetworkTransactionFeeType.CUSTOM) {
|
||||
Keyboard.dismiss();
|
||||
if (feeType === NetworkTransactionFeeType.CUSTOM) {
|
||||
setSelectedFeeType(feeType);
|
||||
return;
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
if (networkFees) {
|
||||
let selectedFee: number;
|
||||
switch (feeType) {
|
||||
case NetworkTransactionFeeType.FAST:
|
||||
selectedFee = networkFees.fastestFee;
|
||||
onFeeSelected(networkFees.fastestFee);
|
||||
break;
|
||||
case NetworkTransactionFeeType.MEDIUM:
|
||||
selectedFee = networkFees.mediumFee;
|
||||
onFeeSelected(networkFees.mediumFee);
|
||||
break;
|
||||
case NetworkTransactionFeeType.SLOW:
|
||||
selectedFee = networkFees.slowFee;
|
||||
break;
|
||||
case NetworkTransactionFeeType.CUSTOM:
|
||||
selectedFee = Number(customFeeValue);
|
||||
onFeeSelected(networkFees.slowFee);
|
||||
break;
|
||||
}
|
||||
onFeeSelected(selectedFee);
|
||||
|
||||
setSelectedFeeType(feeType);
|
||||
}
|
||||
};
|
||||
@ -94,7 +93,8 @@ const ReplaceFeeSuggestions: React.FC<ReplaceFeeSuggestionsProps> = ({ onFeeSele
|
||||
const handleCustomFeeChange = (customFee: string) => {
|
||||
const sanitizedFee = customFee.replace(/[^0-9]/g, '');
|
||||
setCustomFeeValue(sanitizedFee);
|
||||
handleFeeSelection(NetworkTransactionFeeType.CUSTOM);
|
||||
onFeeSelected(Number(sanitizedFee));
|
||||
setSelectedFeeType(NetworkTransactionFeeType.CUSTOM);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -156,7 +156,10 @@ const ReplaceFeeSuggestions: React.FC<ReplaceFeeSuggestionsProps> = ({ onFeeSele
|
||||
ref={customTextInput}
|
||||
maxLength={9}
|
||||
style={[styles.customFeeInput, stylesHook.customFeeInput]}
|
||||
onFocus={() => handleCustomFeeChange(customFeeValue)}
|
||||
onFocus={() => {
|
||||
setSelectedFeeType(NetworkTransactionFeeType.CUSTOM);
|
||||
onFeeSelected(Number(customFeeValue));
|
||||
}}
|
||||
placeholder={loc.send.fee_satvbyte}
|
||||
placeholderTextColor="#81868e"
|
||||
inputAccessoryViewID={DismissKeyboardInputAccessoryViewID}
|
||||
|
||||
@ -7,10 +7,18 @@ 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, ...otherProps } = props;
|
||||
const {
|
||||
style,
|
||||
contentContainerStyle,
|
||||
floatingButtonHeight = 0,
|
||||
headerHeight = 0,
|
||||
disableDefaultTopPadding = false,
|
||||
...otherProps
|
||||
} = props;
|
||||
const { colors } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
@ -32,7 +40,10 @@ const SafeAreaScrollView = forwardRef<ScrollView, SafeAreaScrollViewProps>((prop
|
||||
if (headerHeight > 0) {
|
||||
return headerHeight;
|
||||
}
|
||||
// iOS safe area or no status bar
|
||||
if (disableDefaultTopPadding) {
|
||||
return 0;
|
||||
}
|
||||
// Preserve legacy behavior for existing screens
|
||||
return insets.top > 0 ? 5 : 0;
|
||||
})(),
|
||||
};
|
||||
@ -48,7 +59,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]);
|
||||
}, [insets, contentContainerStyle, floatingButtonHeight, headerHeight, disableDefaultTopPadding]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { TouchableOpacity, Text, StyleSheet, View } from 'react-native';
|
||||
import { TouchableOpacity, Text, StyleSheet, View, useWindowDimensions } from 'react-native';
|
||||
import { useStorage } from '../hooks/context/useStorage';
|
||||
import loc, { formatBalanceWithoutSuffix } from '../loc';
|
||||
import { BitcoinUnit } from '../models/bitcoinUnits';
|
||||
@ -22,6 +22,7 @@ const TotalWalletsBalance: React.FC = React.memo(() => {
|
||||
setTotalBalancePreferredUnitStorage,
|
||||
} = useSettings();
|
||||
const { colors } = useTheme();
|
||||
const { fontScale } = useWindowDimensions();
|
||||
|
||||
const totalBalanceFormatted = useMemo(() => {
|
||||
const totalBalance = wallets.reduce((prev, curr) => {
|
||||
@ -31,6 +32,22 @@ 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(
|
||||
() => [
|
||||
{
|
||||
@ -92,13 +109,20 @@ const TotalWalletsBalance: React.FC = React.memo(() => {
|
||||
|
||||
return (
|
||||
<ToolTipMenu actions={toolTipActions} onPressMenuItem={onPressMenuItem} shouldOpenOnLongPress style={styles.menuContainer}>
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.label}>{loc.wallets.total_balance}</Text>
|
||||
<TouchableOpacity onPress={handleBalanceOnPress}>
|
||||
<Text style={[styles.balance, { color: colors.foregroundColor }]}>
|
||||
{totalBalanceFormatted}{' '}
|
||||
<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}
|
||||
{totalBalancePreferredUnit !== BitcoinUnit.LOCAL_CURRENCY && (
|
||||
<Text style={[styles.currency, { color: colors.foregroundColor }]}>{totalBalancePreferredUnit}</Text>
|
||||
<Text style={[styles.currency, { color: colors.foregroundColor }]}>{` ${totalBalancePreferredUnit}`}</Text>
|
||||
)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@ -116,6 +140,11 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'flex-start',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
width: '100%',
|
||||
},
|
||||
balanceTouchable: {
|
||||
alignSelf: 'stretch',
|
||||
width: '100%',
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
@ -125,6 +154,7 @@ 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 } from 'react-native';
|
||||
import { Animated, Easing, Linking, Pressable, Text, TextStyle, ViewStyle, StyleSheet, View, useWindowDimensions } from 'react-native';
|
||||
import Lnurl from '../class/lnurl';
|
||||
import { LightningArkWallet } from '../class/wallets/lightning-ark-wallet';
|
||||
import { LightningTransaction, Transaction } from '../class/wallets/types';
|
||||
@ -29,9 +29,6 @@ import { uint8ArrayToHex } from '../blue_modules/uint8array-extras';
|
||||
import ListItem from './ListItem';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
dateLine: {
|
||||
fontSize: 13,
|
||||
},
|
||||
fullWidthButton: {
|
||||
width: '100%',
|
||||
alignSelf: 'stretch',
|
||||
@ -133,6 +130,7 @@ 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,
|
||||
@ -248,6 +246,7 @@ 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,
|
||||
@ -262,6 +261,7 @@ 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={<Text style={styles.dateLine}>{dateLine}</Text>}
|
||||
subtitle={dateLine}
|
||||
chevron={false}
|
||||
rightTitle={rowTitle}
|
||||
rightTitleStyle={rowTitleStyle}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import Animated, { useAnimatedStyle, useSharedValue, withSpring, withTiming } from 'react-native-reanimated';
|
||||
import { Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
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,35 +14,39 @@ 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?: (isShouldBeVisible: boolean) => void;
|
||||
onWalletBalanceVisibilityChange?: (shouldHideBalance: 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) {
|
||||
@ -73,13 +77,14 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
||||
|
||||
const handleBalanceVisibility = useCallback(() => {
|
||||
onWalletBalanceVisibilityChange?.(!hideBalance);
|
||||
}, [onWalletBalanceVisibilityChange, hideBalance]);
|
||||
}, [hideBalance, onWalletBalanceVisibilityChange]);
|
||||
|
||||
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) {
|
||||
@ -88,7 +93,6 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
||||
newWalletPreferredUnit = BitcoinUnit.BTC;
|
||||
}
|
||||
|
||||
console.debug('[UnitSwitch/UI] next unit resolved', { walletID: wallet.getID?.(), next: newWalletPreferredUnit });
|
||||
onWalletUnitChange(newWalletPreferredUnit);
|
||||
};
|
||||
|
||||
@ -103,9 +107,9 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
||||
|
||||
const onPressMenuItem = useCallback(
|
||||
(id: string) => {
|
||||
if (id === 'walletBalanceVisibility') {
|
||||
if (id === actionKeys.WalletBalanceVisibility) {
|
||||
handleBalanceVisibility();
|
||||
} else if (id === 'copyToClipboard') {
|
||||
} else if (id === actionKeys.CopyToClipboard) {
|
||||
handleCopyPress();
|
||||
}
|
||||
},
|
||||
@ -140,148 +144,160 @@ 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: 'walletBalanceVisibility',
|
||||
id: actionKeys.WalletBalanceVisibility,
|
||||
text: loc.transactions.details_balance_show,
|
||||
icon: {
|
||||
iconValue: 'eye',
|
||||
},
|
||||
icon: actionIcons.Eye,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: 'walletBalanceVisibility',
|
||||
id: actionKeys.WalletBalanceVisibility,
|
||||
text: loc.transactions.details_balance_hide,
|
||||
icon: {
|
||||
iconValue: 'eye.slash',
|
||||
},
|
||||
icon: actionIcons.EyeSlash,
|
||||
},
|
||||
{
|
||||
id: 'copyToClipboard',
|
||||
id: actionKeys.CopyToClipboard,
|
||||
text: loc.transactions.details_copy,
|
||||
icon: {
|
||||
iconValue: 'doc.on.doc',
|
||||
},
|
||||
icon: actionIcons.Clipboard,
|
||||
},
|
||||
];
|
||||
}, [hideBalance]);
|
||||
|
||||
useEffect(() => {
|
||||
console.debug('[UnitSwitch/UI] render state', {
|
||||
walletID: wallet.getID?.(),
|
||||
unit,
|
||||
hideBalance,
|
||||
preferredFiat: preferredFiatCurrency?.endPointKey,
|
||||
switching: unitSwitching,
|
||||
});
|
||||
}, [wallet, unit, hideBalance, preferredFiatCurrency, unitSwitching]);
|
||||
|
||||
return (
|
||||
<LinearGradient colors={WalletGradient.gradientsFor(wallet.type)} style={styles.lineaderGradient}>
|
||||
<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} />
|
||||
<View style={styles.contentContainer}>
|
||||
<Text testID="WalletLabel" numberOfLines={1} style={[styles.walletLabel, { writingDirection: direction }]}>
|
||||
{wallet.getLabel()}
|
||||
</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
|
||||
<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
|
||||
testID="WalletBalance"
|
||||
numberOfLines={1}
|
||||
minimumFontScale={0.5}
|
||||
adjustsFontSizeToFit
|
||||
style={[styles.walletBalanceText, animatedBalanceTextStyle]}
|
||||
style={styles.walletBalanceText}
|
||||
>
|
||||
{balance}
|
||||
</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>
|
||||
)}
|
||||
</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>
|
||||
{wallet.type === MultisigHDWallet.type && (
|
||||
<TouchableOpacity style={styles.manageFundsButton} accessibilityRole="button" onPress={() => handleManageFundsPressed()}>
|
||||
<Text style={styles.manageFundsButtonText}>{loc.multisig.manage_keys}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</LinearGradient>
|
||||
<View style={styles.bottomBarSpacer}>
|
||||
<View
|
||||
style={[
|
||||
styles.bottomBar,
|
||||
{
|
||||
backgroundColor: colors.background,
|
||||
...Platform.select({
|
||||
ios: { shadowColor: colors.shadowColor },
|
||||
android: {},
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
lineaderGradient: {
|
||||
minHeight: 140,
|
||||
justifyContent: 'flex-start',
|
||||
position: 'relative',
|
||||
},
|
||||
contentContainer: {
|
||||
padding: 15,
|
||||
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,
|
||||
},
|
||||
}),
|
||||
},
|
||||
walletLabel: {
|
||||
backgroundColor: 'transparent',
|
||||
fontSize: 19,
|
||||
color: '#fff',
|
||||
marginBottom: 10,
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
marginBottom: 4,
|
||||
},
|
||||
walletBalance: {
|
||||
flexShrink: 1,
|
||||
marginRight: 6,
|
||||
minHeight: 39,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
balanceSection: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
manageFundsButton: {
|
||||
marginTop: 14,
|
||||
@ -302,13 +318,13 @@ const styles = StyleSheet.create({
|
||||
walletBalanceAndUnitContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingRight: 10, // Ensure there's some padding to the right
|
||||
paddingRight: 10,
|
||||
},
|
||||
walletBalanceText: {
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 36,
|
||||
flexShrink: 1, // Allow the text to shrink if there's not enough space
|
||||
flexShrink: 1,
|
||||
},
|
||||
walletPreferredUnitView: {
|
||||
justifyContent: 'center',
|
||||
|
||||
@ -30,6 +30,7 @@ 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';
|
||||
@ -37,6 +38,30 @@ 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 {
|
||||
@ -160,23 +185,28 @@ 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: {
|
||||
height: 40,
|
||||
minHeight: 40,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
balanceContainerCompact: {
|
||||
height: 32,
|
||||
minHeight: 32,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
image: {
|
||||
width: 99,
|
||||
@ -189,9 +219,6 @@ const iStyles = StyleSheet.create({
|
||||
width: 78,
|
||||
height: 74,
|
||||
},
|
||||
br: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
label: {
|
||||
backgroundColor: 'transparent',
|
||||
fontSize: 19,
|
||||
@ -206,7 +233,6 @@ const iStyles = StyleSheet.create({
|
||||
},
|
||||
balanceCompact: {
|
||||
fontSize: 28,
|
||||
lineHeight: 34,
|
||||
},
|
||||
latestTx: {
|
||||
backgroundColor: 'transparent',
|
||||
@ -282,11 +308,32 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
||||
const balanceOpacity = useSharedValue(1);
|
||||
const balanceTranslateY = useSharedValue(0);
|
||||
const { colors } = useTheme();
|
||||
const { width } = useWindowDimensions();
|
||||
const { width, fontScale } = 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;
|
||||
@ -431,23 +478,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]}>
|
||||
<LinearGradient
|
||||
colors={WalletGradient.gradientsFor(item.type)}
|
||||
style={[iStyles.grad, isCompact && iStyles.gradCompact, scaledCardStyles.grad]}
|
||||
>
|
||||
<ImageBackground source={image} style={[iStyles.image, isCompact && iStyles.imageCompact]} />
|
||||
<View style={[iStyles.gradContent, isCompact && iStyles.gradContentCompact]}>
|
||||
<Text style={iStyles.br} />
|
||||
<View style={[iStyles.gradContent, isCompact && iStyles.gradContentCompact, !isCompact && scaledCardStyles.gradContent]}>
|
||||
{!isPlaceHolder && (
|
||||
<>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
iStyles.label,
|
||||
isCompact && iStyles.labelCompact,
|
||||
{ color: colors.inverseForegroundColor, writingDirection: direction },
|
||||
]}
|
||||
style={[iStyles.label, isCompact && iStyles.labelCompact, scaledCardStyles.label, cardTextStyle]}
|
||||
>
|
||||
{renderHighlightedText ? renderHighlightedText(walletLabel, searchQuery || '') : walletLabel}
|
||||
</Text>
|
||||
<View style={[iStyles.balanceContainer, isCompact && iStyles.balanceContainerCompact]}>
|
||||
<View
|
||||
style={[iStyles.balanceContainer, isCompact && iStyles.balanceContainerCompact, scaledCardStyles.balanceContainer]}
|
||||
>
|
||||
{hideBalance ? (
|
||||
<>
|
||||
<BlueSpacing10 />
|
||||
@ -457,11 +504,13 @@ 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,
|
||||
{ color: colors.inverseForegroundColor, writingDirection: direction },
|
||||
isCompact ? scaledCardStyles.balanceCompact : scaledCardStyles.balance,
|
||||
cardTextStyle,
|
||||
animatedBalanceStyle,
|
||||
]}
|
||||
>
|
||||
@ -469,24 +518,20 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
||||
</Animated.Text>
|
||||
)}
|
||||
</View>
|
||||
<Text style={iStyles.br} />
|
||||
<View style={scaledCardStyles.textSpacer} />
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
iStyles.latestTx,
|
||||
isCompact && iStyles.latestTxCompact,
|
||||
{ color: colors.inverseForegroundColor, writingDirection: direction },
|
||||
]}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.8}
|
||||
style={[iStyles.latestTx, isCompact && iStyles.latestTxCompact, scaledCardStyles.latestTx, cardTextStyle]}
|
||||
>
|
||||
{loc.wallets.list_latest_transaction}
|
||||
</Text>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
iStyles.latestTxTime,
|
||||
isCompact && iStyles.latestTxTimeCompact,
|
||||
{ color: colors.inverseForegroundColor, writingDirection: direction },
|
||||
]}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.8}
|
||||
style={[iStyles.latestTxTime, isCompact && iStyles.latestTxTimeCompact, scaledCardStyles.latestTxTime, cardTextStyle]}
|
||||
>
|
||||
{latestTransactionText}
|
||||
</Text>
|
||||
@ -515,15 +560,7 @@ interface WalletsCarouselProps extends Partial<FlatListProps<any>> {
|
||||
animateChanges?: boolean;
|
||||
}
|
||||
|
||||
type FlatListRefType = FlatList<any> & {
|
||||
scrollToEnd(params?: { animated?: boolean | null }): void;
|
||||
scrollToIndex(params: { animated?: boolean | null; index: number; viewOffset?: number; viewPosition?: number }): void;
|
||||
scrollToItem(params: { animated?: boolean | null; item: TWallet; viewPosition?: number }): void;
|
||||
scrollToOffset(params: { animated?: boolean | null; offset: number }): void;
|
||||
recordInteraction(): void;
|
||||
flashScrollIndicators(): void;
|
||||
getNativeScrollRef(): View;
|
||||
};
|
||||
export type CarouselListRefType = FlatList<TWallet>;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listHeaderSeparator: {
|
||||
@ -534,7 +571,7 @@ const styles = StyleSheet.create({
|
||||
|
||||
const ListHeaderSeparator = () => <View style={styles.listHeaderSeparator} />;
|
||||
|
||||
const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props, ref) => {
|
||||
const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((props, ref) => {
|
||||
const {
|
||||
horizontal = true,
|
||||
data,
|
||||
@ -549,7 +586,7 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
||||
animateChanges = false,
|
||||
} = props;
|
||||
|
||||
const { width } = useWindowDimensions();
|
||||
const { width, fontScale } = useWindowDimensions();
|
||||
const itemWidth = React.useMemo(() => getWalletCarouselItemWidth(width), [width]);
|
||||
const snapInterval = React.useMemo(() => itemWidth, [itemWidth]);
|
||||
const snapOffsets = React.useMemo(() => {
|
||||
@ -569,7 +606,7 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
||||
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
const flatListRef = useRef<FlatList<any>>(null);
|
||||
const flatListRef = useRef<FlatList<TWallet>>(null);
|
||||
const walletRefs = useRef<Record<string, React.MutableRefObject<View | null>>>({});
|
||||
|
||||
const { sizeClass } = useSizeClass();
|
||||
@ -658,7 +695,7 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
||||
console.warn('[WalletsCarousel] Error scrolling to wallet:', error);
|
||||
// Fallback: try scrolling to offset
|
||||
// Use different measurement based on orientation
|
||||
const itemSize = horizontal ? itemWidth : 195; // 195 is the approximate height of wallet card
|
||||
const itemSize = horizontal ? itemWidth : WALLET_CAROUSEL_HEIGHT;
|
||||
flatListRef.current.scrollToOffset({
|
||||
offset: itemSize * walletIndex,
|
||||
animated,
|
||||
@ -780,7 +817,7 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
||||
|
||||
const keyExtractor = useCallback((item: TWallet, index: number) => (item?.getID ? item.getID() : index.toString()), []);
|
||||
|
||||
const sliderHeight = 195;
|
||||
const sliderHeight = getWalletCarouselHeight(fontScale);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@ -863,7 +900,8 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
||||
|
||||
const cStyles = StyleSheet.create({
|
||||
content: {
|
||||
paddingTop: 16,
|
||||
paddingTop: scaleLayoutUp(WALLET_CAROUSEL_PADDING_TOP, fontScale),
|
||||
paddingBottom: scaleLayoutUp(WALLET_CAROUSEL_PADDING_BOTTOM, fontScale),
|
||||
},
|
||||
contentLargeScreen: {
|
||||
paddingHorizontal: sizeClass === SizeClass.Large ? 16 : 12,
|
||||
@ -894,7 +932,7 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
||||
automaticallyAdjustContentInsets
|
||||
automaticallyAdjustKeyboardInsets
|
||||
automaticallyAdjustsScrollIndicatorInsets
|
||||
style={{ minHeight: sliderHeight + 12 }}
|
||||
style={{ minHeight: sliderHeight }}
|
||||
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||
ListFooterComponent={onNewWalletPress ? <NewWalletPanel onPress={onNewWalletPress} /> : null}
|
||||
{...props}
|
||||
|
||||
@ -31,6 +31,8 @@ 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,
|
||||
@ -107,6 +109,15 @@ 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,
|
||||
@ -192,6 +203,7 @@ export const SettingsScrollView = forwardRef<ScrollView, SettingsScrollViewProps
|
||||
ref={ref}
|
||||
style={[style, { backgroundColor: screenBackgroundColor }]}
|
||||
headerHeight={resolvedHeaderHeight}
|
||||
disableDefaultTopPadding={isIOS26OrHigher}
|
||||
floatingButtonHeight={floatingButtonHeight}
|
||||
contentContainerStyle={[staticStyles.contentContainer, contentContainerStyle]}
|
||||
{...rest}
|
||||
|
||||
@ -14,6 +14,8 @@ 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',
|
||||
@ -101,6 +103,7 @@ 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,8 +245,6 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>UIDesignRequiresCompatibility</key>
|
||||
<true/>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
PODS:
|
||||
- BugsnagReactNative (8.8.1):
|
||||
- BugsnagReactNative (8.9.0):
|
||||
- hermes-engine
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
@ -29,7 +29,7 @@ PODS:
|
||||
- hermes-engine/Pre-built (= 250829098.0.10)
|
||||
- hermes-engine/Pre-built (250829098.0.10)
|
||||
- lottie-ios (4.6.0)
|
||||
- lottie-react-native (7.3.7):
|
||||
- lottie-react-native (7.3.8):
|
||||
- hermes-engine
|
||||
- lottie-ios (= 4.6.0)
|
||||
- RCTRequired
|
||||
@ -2018,6 +2018,8 @@ PODS:
|
||||
- ReactNativeDependencies (0.85.3)
|
||||
- RealmJS (20.2.0):
|
||||
- React
|
||||
- RNBackgroundFetch (4.2.9):
|
||||
- React-Core
|
||||
- RNCAsyncStorage (2.2.0):
|
||||
- hermes-engine
|
||||
- RCTRequired
|
||||
@ -2543,6 +2545,7 @@ DEPENDENCIES:
|
||||
- ReactNativeCameraKit (from `../node_modules/react-native-camera-kit-no-google`)
|
||||
- ReactNativeDependencies (from `../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`)
|
||||
- RealmJS (from `../node_modules/realm`)
|
||||
- RNBackgroundFetch (from `../node_modules/react-native-background-fetch`)
|
||||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
|
||||
- RNDefaultPreference (from `../node_modules/react-native-default-preference`)
|
||||
@ -2762,6 +2765,8 @@ EXTERNAL SOURCES:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec"
|
||||
RealmJS:
|
||||
:path: "../node_modules/realm"
|
||||
RNBackgroundFetch:
|
||||
:path: "../node_modules/react-native-background-fetch"
|
||||
RNCAsyncStorage:
|
||||
:path: "../node_modules/@react-native-async-storage/async-storage"
|
||||
RNCClipboard:
|
||||
@ -2802,13 +2807,13 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
BugsnagReactNative: bee770e3f497a8571feb1579bdc083a070bee1f3
|
||||
BugsnagReactNative: 73ce58aac04585e7cba3081c0abba06d848d62fc
|
||||
BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70
|
||||
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
|
||||
FBLazyVector: 24e62c765683b8d89006a88a2c8f5cf019f0074d
|
||||
hermes-engine: 86cdbf283775c54dc008895c3eacd24a1f2a40b4
|
||||
hermes-engine: 4ed74710a31e8e31f20356c641eab1d8f7d54595
|
||||
lottie-ios: 8f959969761e9c45d70353667d00af0e5b9cadb3
|
||||
lottie-react-native: 26b365c3d5615e87f4db048dcb151de3eb9a8e76
|
||||
lottie-react-native: ee142214581f3bb68fbda7efcf07b835a189eeda
|
||||
RCTDeprecation: a4c521821fab57cbb125b36effe84d897d0dfa12
|
||||
RCTRequired: 9f3a7e5645d4bc3f551593de7550bb66ab6e42bc
|
||||
RCTSwiftUI: 239ed2eb9e73de5a6f518810630f0c95e01c8702
|
||||
@ -2817,7 +2822,7 @@ SPEC CHECKSUMS:
|
||||
React: e2dc35338068bbd299c66f043ae0d7f25de8499e
|
||||
React-callinvoker: 28b25d21b124c26cebaea713ba7d801b9351dc48
|
||||
React-Core: 02ed7d2ffb70437bdf2aba074a13078a7b0b9ff0
|
||||
React-Core-prebuilt: 9e875134f667c471ab68bf9edf1661fa11b86540
|
||||
React-Core-prebuilt: 3445f1028d9b206cd45c8bbb7e2427ee891f810e
|
||||
React-CoreModules: b3a5a42dadcde3b5d47b325bd912eb2ced89e146
|
||||
React-cxxreact: fe8f88dda044e5905e99a00f41b7a874c3908716
|
||||
React-debug: 92944dc4d89f56d640e75498266cbde557a48189
|
||||
@ -2898,8 +2903,9 @@ SPEC CHECKSUMS:
|
||||
ReactCodegen: 1bd7f2174582b0e142f8671735b5c906c08b72ea
|
||||
ReactCommon: 7dfc3250793bf36cf221096ff59e1179e13eef7f
|
||||
ReactNativeCameraKit: 5974256fc608631c1c812710cd98abe95dae0f88
|
||||
ReactNativeDependencies: 0a5c93845772e4b1c5ad065c59a859518b13a6b7
|
||||
ReactNativeDependencies: 75299c281f422106c723e79dc1f6ce7ef03241be
|
||||
RealmJS: 1c37c6bdfe060f4caa0f9175aa0eedb962622ee1
|
||||
RNBackgroundFetch: 64b1215fbb8ec58afba877ca0ce177e009ce12b7
|
||||
RNCAsyncStorage: 2ad919e88b8bc2cd80e8697ce66d04d006743283
|
||||
RNCClipboard: 715fa7c6c8366f17d00f05a439ee7488f390fa5f
|
||||
RNDefaultPreference: 8a089ee8ce829a66c5453e3c5434f0785499d1c3
|
||||
|
||||
@ -288,7 +288,6 @@
|
||||
"general": "General",
|
||||
"general_continuity": "Continuity",
|
||||
"general_continuity_e": "When enabled, you will be able to view selected wallets, and transactions, using your other Apple iCloud connected devices.",
|
||||
"groundcontrol_explanation": "GroundControl is a free, open-source push notifications server for Bitcoin wallets. You can install your own GroundControl server and put its URL here to not rely on BlueWallet’s infrastructure. Leave blank to use GroundControl’s default server.",
|
||||
"header": "Settings",
|
||||
"language": "Language",
|
||||
"last_updated": "Last Updated",
|
||||
@ -304,7 +303,6 @@
|
||||
"network_broadcast": "Broadcast Transaction",
|
||||
"network_electrum": "Electrum Server",
|
||||
"electrum_suggested_description": "When a preferred server is not set, a suggested server will be selected for use at random.",
|
||||
"not_a_valid_uri": "Invalid URI",
|
||||
"notifications": "Notifications",
|
||||
"open_link_in_explorer": "Open link in explorer",
|
||||
"password": "Password",
|
||||
@ -322,7 +320,6 @@
|
||||
"push_notifications_explanation": "By enabling notifications, your device token will be sent to the server, along with wallet addresses and transaction IDs for all wallets and transactions made after enabling notifications. The device token is used to send notifications, and the wallet information allows us to notify you about incoming Bitcoin or transaction confirmations.\n\nOnly information from after you enable notifications is transmitted—nothing from before is collected.\n\nDisabling notifications will remove all of this information from the server. Additionally, deleting a wallet from the app will also remove its associated information from the server.",
|
||||
"selfTest": "Self-Test",
|
||||
"save": "Save",
|
||||
"saved": "Saved",
|
||||
"success_transaction_broadcasted": "Your transaction has been successfully broadcasted!",
|
||||
"total_balance": "Total Balance",
|
||||
"total_balance_explanation": "Display the total balance of all your wallets on your home screen widgets.",
|
||||
|
||||
@ -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 { NativeStackNavigationOptions } from '@react-navigation/native-stack';
|
||||
import type { NativeStackHeaderItem, NativeStackNavigationOptions } from '@react-navigation/native-stack';
|
||||
import navigationStyle, { CloseButtonPosition } from '../components/navigationStyle';
|
||||
import { useTheme } from '../components/themes';
|
||||
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
|
||||
@ -57,6 +57,9 @@ 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);
|
||||
@ -150,6 +153,15 @@ const DetailViewStackScreensStack = () => {
|
||||
navigation.navigate('AddWalletRoot');
|
||||
}, [navigation]);
|
||||
|
||||
const navigateToSettings = useCallback(() => {
|
||||
navigation.navigate('DrawerRoot', {
|
||||
screen: 'DetailViewStackScreensStack',
|
||||
params: {
|
||||
screen: 'Settings',
|
||||
},
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
const RightBarButtons = useMemo(
|
||||
() =>
|
||||
sizeClass === SizeClass.Large ? (
|
||||
@ -219,6 +231,53 @@ 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,
|
||||
@ -233,6 +292,7 @@ const DetailViewStackScreensStack = () => {
|
||||
RightBarButtons,
|
||||
sizeClass,
|
||||
theme.colors.customHeader,
|
||||
theme.colors.headerProminentButtonBackgroundColor,
|
||||
theme.colors.foregroundColor,
|
||||
theme.colors.lightButton,
|
||||
theme.colors.redBG,
|
||||
@ -242,6 +302,8 @@ const DetailViewStackScreensStack = () => {
|
||||
electrumConnected,
|
||||
isElectrumDisabled,
|
||||
navigateToElectrumSettings,
|
||||
navigateToAddWallet,
|
||||
navigateToSettings,
|
||||
walletTransactionUpdateStatus,
|
||||
]);
|
||||
|
||||
@ -251,6 +313,14 @@ 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
|
||||
@ -273,6 +343,9 @@ const DetailViewStackScreensStack = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const settingsScreenOptions = (title: string) =>
|
||||
isIOS26OrHigher ? getSettingsHeaderOptions(title) : navigationStyle(getSettingsHeaderOptions(title))(theme);
|
||||
|
||||
return (
|
||||
<ConnectionPollContext.Provider value={connectionPollContextValue}>
|
||||
<DetailViewStack.Navigator
|
||||
@ -339,22 +412,14 @@ const DetailViewStackScreensStack = () => {
|
||||
options={navigationStyle({ title: loc.lndViewInvoice.additional_info })(theme)}
|
||||
/>
|
||||
|
||||
<DetailViewStack.Screen
|
||||
name="Broadcast"
|
||||
component={Broadcast}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.send.create_broadcast))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen name="Broadcast" component={Broadcast} options={settingsScreenOptions(loc.send.create_broadcast)} />
|
||||
<DetailViewStack.Screen
|
||||
name="IsItMyAddress"
|
||||
component={IsItMyAddress}
|
||||
initialParams={{ address: undefined }}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.is_it_my_address.title))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="GenerateWord"
|
||||
component={GenerateWord}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.autofill_word.title))(theme)}
|
||||
options={settingsScreenOptions(loc.is_it_my_address.title)}
|
||||
/>
|
||||
<DetailViewStack.Screen name="GenerateWord" component={GenerateWord} options={settingsScreenOptions(loc.autofill_word.title)} />
|
||||
<DetailViewStack.Screen
|
||||
name="LnurlPay"
|
||||
component={LnurlPay}
|
||||
@ -397,115 +462,90 @@ const DetailViewStackScreensStack = () => {
|
||||
<DetailViewStack.Screen
|
||||
name="Settings"
|
||||
component={Settings}
|
||||
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'
|
||||
? {
|
||||
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: {
|
||||
color:
|
||||
typeof theme.colors.foregroundColor === 'string'
|
||||
? theme.colors.foregroundColor
|
||||
: String(theme.colors.foregroundColor),
|
||||
}
|
||||
: 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)}
|
||||
},
|
||||
headerTransparent: false,
|
||||
headerBlurEffect: undefined,
|
||||
headerStyle: {
|
||||
backgroundColor: settingsHeaderBackgroundColor,
|
||||
},
|
||||
animationTypeForReplace: 'push',
|
||||
})(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={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)}
|
||||
options={settingsScreenOptions(loc.plausibledeniability.title)}
|
||||
/>
|
||||
<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={navigationStyle(getSettingsHeaderOptions(loc.settings.block_explorer))(theme)}
|
||||
options={settingsScreenOptions(loc.settings.block_explorer)}
|
||||
/>
|
||||
|
||||
<DetailViewStack.Screen
|
||||
name="About"
|
||||
component={About}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.about))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen name="About" component={About} options={settingsScreenOptions(loc.settings.about)} />
|
||||
{/* <DetailViewStack.Screen
|
||||
name="DefaultView"
|
||||
component={DefaultView}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.default_title))(theme)}
|
||||
options={settingsScreenOptions(loc.settings.default_title)}
|
||||
/> */}
|
||||
<DetailViewStack.Screen
|
||||
name="ElectrumSettings"
|
||||
component={ElectrumSettings}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.electrum_settings_server))(theme)}
|
||||
options={settingsScreenOptions(loc.settings.electrum_settings_server)}
|
||||
initialParams={{ server: undefined }}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="EncryptStorage"
|
||||
component={EncryptStorage}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.encrypt_title))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="Language"
|
||||
component={Language}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.language))(theme)}
|
||||
options={settingsScreenOptions(loc.settings.encrypt_title)}
|
||||
/>
|
||||
<DetailViewStack.Screen name="Language" component={Language} options={settingsScreenOptions(loc.settings.language)} />
|
||||
<DetailViewStack.Screen
|
||||
name="LightningSettings"
|
||||
component={LightningSettings}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.lightning_settings))(theme)}
|
||||
options={settingsScreenOptions(loc.settings.lightning_settings)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="NotificationSettings"
|
||||
component={NotificationSettings}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.notifications))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="SelfTest"
|
||||
component={SelfTest}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.selfTest))(theme)}
|
||||
options={settingsScreenOptions(loc.settings.notifications)}
|
||||
/>
|
||||
<DetailViewStack.Screen name="SelfTest" component={SelfTest} options={settingsScreenOptions(loc.settings.selfTest)} />
|
||||
<DetailViewStack.Screen
|
||||
name="ReleaseNotes"
|
||||
component={ReleaseNotes}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.about_release_notes))(theme)}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="SettingsTools"
|
||||
component={SettingsTools}
|
||||
options={navigationStyle(getSettingsHeaderOptions(loc.settings.tools))(theme)}
|
||||
options={settingsScreenOptions(loc.settings.about_release_notes)}
|
||||
/>
|
||||
<DetailViewStack.Screen name="SettingsTools" component={SettingsTools} options={settingsScreenOptions(loc.settings.tools)} />
|
||||
<DetailViewStack.Screen
|
||||
name="PromptPasswordConfirmationSheet"
|
||||
component={PromptPasswordConfirmationSheet}
|
||||
|
||||
@ -1,44 +1,98 @@
|
||||
import React from 'react';
|
||||
import { TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { Platform, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import type { NativeStackHeaderItem, NativeStackNavigationOptions } from '@react-navigation/native-stack';
|
||||
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 getWalletTransactionsOptions = ({ route }: { route: WalletTransactionsRouteProps }): NativeStackNavigationOptions => {
|
||||
const { isLoading = false, walletID, walletType } = route.params;
|
||||
const HERO_HEADER_ICON_COLOR = '#FFFFFF';
|
||||
|
||||
const onPress = () => {
|
||||
navigationRef.navigate('WalletDetails', {
|
||||
walletID,
|
||||
});
|
||||
};
|
||||
const navigateToWalletDetails = (walletID: string) => {
|
||||
navigationRef.navigate('WalletDetails', {
|
||||
walletID,
|
||||
});
|
||||
};
|
||||
|
||||
const RightButton = (
|
||||
<TouchableOpacity accessibilityRole="button" testID="WalletDetails" disabled={isLoading} style={styles.walletDetails} onPress={onPress}>
|
||||
<Icon name="more-horiz" type="material" size={22} color="#FFFFFF" />
|
||||
/** 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} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const backgroundColor = WalletGradient.headerColorFor(walletType);
|
||||
/** 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,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
return {
|
||||
const getWalletTransactionsOptions = ({ route }: { route: WalletTransactionsRouteProps }): NativeStackNavigationOptions => {
|
||||
const { isLoading = false, walletID } = route.params;
|
||||
|
||||
const base: NativeStackNavigationOptions = {
|
||||
title: '',
|
||||
headerBackTitleStyle: { fontSize: 0 },
|
||||
headerTransparent: true,
|
||||
headerStyle: {
|
||||
backgroundColor,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
headerBackButtonDisplayMode: 'minimal',
|
||||
headerShadowVisible: false,
|
||||
headerTintColor: '#FFFFFF',
|
||||
headerTintColor: HERO_HEADER_ICON_COLOR,
|
||||
headerBlurEffect: undefined,
|
||||
statusBarStyle: 'light',
|
||||
headerBackTitle: undefined,
|
||||
headerRight: () => RightButton,
|
||||
headerRight: createWalletDetailsHeaderRight({ walletID, isLoading, iconColor: HERO_HEADER_ICON_COLOR }),
|
||||
};
|
||||
|
||||
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({
|
||||
|
||||
368
package-lock.json
generated
368
package-lock.json
generated
@ -10,35 +10,36 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@arkade-os/boltz-swap": "0.3.37",
|
||||
"@arkade-os/sdk": "0.4.32",
|
||||
"@arkade-os/boltz-swap": "0.3.40",
|
||||
"@arkade-os/sdk": "0.4.35",
|
||||
"@babel/preset-env": "7.29.5",
|
||||
"@bugsnag/react-native": "8.9.0",
|
||||
"@bugsnag/source-maps": "2.3.3",
|
||||
"@keystonehq/bc-ur-registry": "0.7.1",
|
||||
"@ngraveio/bc-ur": "1.1.13",
|
||||
"@noble/hashes": "1.3.3",
|
||||
"@noble/secp256k1": "1.6.3",
|
||||
"@noble/ciphers": "1.3.0",
|
||||
"@noble/hashes": "1.8.0",
|
||||
"@noble/secp256k1": "3.1.0",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-clipboard/clipboard": "1.16.3",
|
||||
"@react-native-community/cli": "20.1.3",
|
||||
"@react-native-community/cli-platform-android": "20.1.3",
|
||||
"@react-native-community/cli-platform-ios": "20.1.3",
|
||||
"@react-native-documents/picker": "12.0.1",
|
||||
"@react-native-vector-icons/entypo": "13.1.1",
|
||||
"@react-native-vector-icons/fontawesome": "13.1.1",
|
||||
"@react-native-vector-icons/fontawesome6": "13.1.1",
|
||||
"@react-native-vector-icons/ionicons": "13.1.1",
|
||||
"@react-native-vector-icons/material-design-icons": "13.1.1",
|
||||
"@react-native-vector-icons/material-icons": "13.1.1",
|
||||
"@react-native-vector-icons/entypo": "13.1.2",
|
||||
"@react-native-vector-icons/fontawesome": "13.1.2",
|
||||
"@react-native-vector-icons/fontawesome6": "13.1.2",
|
||||
"@react-native-vector-icons/ionicons": "13.1.2",
|
||||
"@react-native-vector-icons/material-design-icons": "13.1.2",
|
||||
"@react-native-vector-icons/material-icons": "13.1.2",
|
||||
"@react-native/babel-preset": "0.85.3",
|
||||
"@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.58",
|
||||
"@react-navigation/drawer": "7.10.2",
|
||||
"@react-navigation/native": "7.2.4",
|
||||
"@react-navigation/native-stack": "7.15.1",
|
||||
"@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",
|
||||
"@scure/base": "2.0.0",
|
||||
"@spsina/bip47": "github:BlueWallet/bip47#df82345",
|
||||
"aezeed": "0.0.5",
|
||||
@ -57,8 +58,7 @@
|
||||
"buffer": "6.0.3",
|
||||
"coinselect": "github:BlueWallet/coinselect#35f8038",
|
||||
"crypto-browserify": "3.12.1",
|
||||
"crypto-js": "4.2.0",
|
||||
"dayjs": "1.11.20",
|
||||
"dayjs": "1.11.21",
|
||||
"detox": "20.51.3",
|
||||
"ecpair": "3.0.1",
|
||||
"electrum-client": "github:BlueWallet/rn-electrum-client#83420b8",
|
||||
@ -92,7 +92,7 @@
|
||||
"react-native-linear-gradient": "2.8.3",
|
||||
"react-native-localize": "3.7.0",
|
||||
"react-native-notifications": "5.2.2",
|
||||
"react-native-permissions": "5.5.1",
|
||||
"react-native-permissions": "5.5.3",
|
||||
"react-native-prompt-android": "github:BlueWallet/react-native-prompt-android#ed168d66fed556bc2ed07cf498770f058b78a376",
|
||||
"react-native-quick-actions": "0.3.13",
|
||||
"react-native-reanimated": "4.3.1",
|
||||
@ -115,19 +115,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@jest/reporters": "^27.5.1",
|
||||
"@react-native/eslint-config": "^0.85.3",
|
||||
"@react-native/jest-preset": "0.85.3",
|
||||
"@react-native/js-polyfills": "^0.85.3",
|
||||
"@react-native/js-polyfills": "^0.86.0",
|
||||
"@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",
|
||||
@ -180,12 +178,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@arkade-os/boltz-swap": {
|
||||
"version": "0.3.37",
|
||||
"resolved": "https://registry.npmjs.org/@arkade-os/boltz-swap/-/boltz-swap-0.3.37.tgz",
|
||||
"integrity": "sha512-wP4daP/sDpUahmivaIZC8Lfvqz4lhQMWM1R8/Ib5x7NMS6k++FSs4KKQ6wjPKpweF8ULilsJdorhmLpNlEba6A==",
|
||||
"version": "0.3.40",
|
||||
"resolved": "https://registry.npmjs.org/@arkade-os/boltz-swap/-/boltz-swap-0.3.40.tgz",
|
||||
"integrity": "sha512-Q1myKKXC5c44wzAD6eb4lrq3rro0qwyJqNqf0powjfbhSTzHfk5Do6DfZYrciueEK4agilynLNurWCYsoE8yEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@arkade-os/sdk": "0.4.32",
|
||||
"@arkade-os/sdk": "0.4.35",
|
||||
"@noble/curves": "2.0.1",
|
||||
"@noble/hashes": "2.0.1",
|
||||
"@scure/base": "2.0.0",
|
||||
@ -208,6 +206,8 @@
|
||||
},
|
||||
"node_modules/@arkade-os/boltz-swap/node_modules/@noble/hashes": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
|
||||
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
@ -217,9 +217,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@arkade-os/sdk": {
|
||||
"version": "0.4.32",
|
||||
"resolved": "https://registry.npmjs.org/@arkade-os/sdk/-/sdk-0.4.32.tgz",
|
||||
"integrity": "sha512-we7eNPuuW9PWRS/B4Nlw5MHXTgJ7CuQzbdSrisH0u3P2PPQd/0FbSspEW/OQRNjMrJl+29zAEKN5kswy9MTjxA==",
|
||||
"version": "0.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@arkade-os/sdk/-/sdk-0.4.35.tgz",
|
||||
"integrity": "sha512-gMARWDEgy5YL15vE4hBoUf4IGBi94tDRymtVwIehL+2MQylFm6cO1Qt50/aA6dwle5Ae+XMfF99Wf6k/Gc257A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bitcoinerlab/descriptors-scure": "3.1.7",
|
||||
@ -584,7 +584,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
|
||||
"version": "7.28.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
@ -599,7 +598,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -613,7 +611,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -629,7 +626,6 @@
|
||||
"version": "7.29.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz",
|
||||
"integrity": "sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
@ -644,7 +640,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
@ -660,7 +655,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
@ -688,7 +682,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-proposal-private-property-in-object": {
|
||||
"version": "7.21.0-placeholder-for-preset-env.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -782,7 +775,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-import-assertions": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@ -796,7 +788,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-import-attributes": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@ -950,7 +941,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-unicode-sets-regex": {
|
||||
"version": "7.18.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
|
||||
@ -1008,7 +998,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-block-scoped-functions": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1049,7 +1038,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-class-static-block": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.28.6",
|
||||
@ -1082,7 +1070,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-computed-properties": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
@ -1111,7 +1098,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-dotall-regex": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
@ -1126,7 +1112,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-duplicate-keys": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1140,7 +1125,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
|
||||
"version": "7.29.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
@ -1155,7 +1139,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-dynamic-import": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1169,7 +1152,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-explicit-resource-management": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
@ -1184,7 +1166,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-exponentiation-operator": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@ -1198,7 +1179,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-export-namespace-from": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1240,7 +1220,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-function-name": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-compilation-targets": "^7.27.1",
|
||||
@ -1256,7 +1235,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-json-strings": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@ -1270,7 +1248,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-literals": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1284,7 +1261,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-logical-assignment-operators": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@ -1298,7 +1274,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-member-expression-literals": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1312,7 +1287,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-amd": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.27.1",
|
||||
@ -1343,7 +1317,6 @@
|
||||
"version": "7.29.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz",
|
||||
"integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.28.6",
|
||||
@ -1360,7 +1333,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-umd": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.27.1",
|
||||
@ -1389,7 +1361,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-new-target": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1416,7 +1387,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-numeric-separator": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6"
|
||||
@ -1430,7 +1400,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-object-rest-spread": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-compilation-targets": "^7.28.6",
|
||||
@ -1448,7 +1417,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-object-super": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
@ -1490,7 +1458,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-parameters": {
|
||||
"version": "7.27.7",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1533,7 +1500,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-property-literals": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1616,7 +1582,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-regexp-modifiers": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
@ -1631,7 +1596,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-reserved-words": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1676,7 +1640,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-spread": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.28.6",
|
||||
@ -1691,7 +1654,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-sticky-regex": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1718,7 +1680,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-typeof-symbol": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1749,7 +1710,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-escapes": {
|
||||
"version": "7.27.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
@ -1763,7 +1723,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-property-regex": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
@ -1792,7 +1751,6 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-sets-regex": {
|
||||
"version": "7.28.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||
@ -1809,7 +1767,6 @@
|
||||
"version": "7.29.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.5.tgz",
|
||||
"integrity": "sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.29.3",
|
||||
@ -1895,7 +1852,6 @@
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz",
|
||||
"integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.8",
|
||||
@ -1907,7 +1863,6 @@
|
||||
},
|
||||
"node_modules/@babel/preset-modules": {
|
||||
"version": "0.1.6-no-external-plugins",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.0.0",
|
||||
@ -3541,6 +3496,18 @@
|
||||
"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",
|
||||
@ -3569,24 +3536,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.3.3",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/secp256k1": {
|
||||
"version": "1.6.3",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.1.0.tgz",
|
||||
"integrity": "sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
@ -3890,12 +3858,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-vector-icons/common": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/common/-/common-13.0.0.tgz",
|
||||
"integrity": "sha512-FJ0Ql5UTGVtK0ak4vLTxmhFHadb8NmTk4yOWoggh7UvC2pVQNyJK7L9nIZeIZ0IaVJtKfmKXtBWA0nKqqzQ/FQ==",
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/common/-/common-13.0.1.tgz",
|
||||
"integrity": "sha512-UPC6L3tW5rXCjBn4kgw9RPURUILIg8tFpEY2uaYwU8aCjEHkywNCMcAO8+PvMCDkR6aICPeHYA0OXvMgrjsF4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"find-up": "^7.0.0",
|
||||
"find-up": "^8.0.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"plist": "^3.1.0"
|
||||
},
|
||||
@ -3908,6 +3876,7 @@
|
||||
"peerDependencies": {
|
||||
"@react-native-vector-icons/get-image": "^13.0.0",
|
||||
"@react-native/assets-registry": "*",
|
||||
"expo-font": "*",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
},
|
||||
@ -3917,36 +3886,38 @@
|
||||
},
|
||||
"@react-native/assets-registry": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-font": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-vector-icons/common/node_modules/find-up": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz",
|
||||
"integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==",
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-8.0.0.tgz",
|
||||
"integrity": "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^7.2.0",
|
||||
"path-exists": "^5.0.0",
|
||||
"unicorn-magic": "^0.1.0"
|
||||
"locate-path": "^8.0.0",
|
||||
"unicorn-magic": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-vector-icons/common/node_modules/locate-path": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz",
|
||||
"integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==",
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-8.0.0.tgz",
|
||||
"integrity": "sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
@ -3982,15 +3953,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-vector-icons/common/node_modules/path-exists": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
|
||||
"integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-vector-icons/common/node_modules/yocto-queue": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
|
||||
@ -4004,12 +3966,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-vector-icons/entypo": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/entypo/-/entypo-13.1.1.tgz",
|
||||
"integrity": "sha512-K3uZ/S0Nr0a/vuXw81tZDhKJaUfaGeTG+50vPHO60Ucl/L9b3O4KUtzMJa7zd0c400CO0vl5Lr97Wk266eXwLQ==",
|
||||
"version": "13.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/entypo/-/entypo-13.1.2.tgz",
|
||||
"integrity": "sha512-oxfKPz8amwmI/IiYadwgKlGBo4y68bwYVhx5N4dTffaIR4n73Lk6AUlNUcYzSoMzSAYZVfySGPq7YV8whrc8dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-native-vector-icons/common": "^13.0.0"
|
||||
"@react-native-vector-icons/common": "^13.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
@ -4026,12 +3988,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-vector-icons/fontawesome": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/fontawesome/-/fontawesome-13.1.1.tgz",
|
||||
"integrity": "sha512-GD1eOt1YmkxbUmHZzxpCGMMC3WCif3edo8RKMnv0dlf07KNLktfQDh0mVYJhU4d203oyeTk1E5GWBjNDRw3zWg==",
|
||||
"version": "13.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/fontawesome/-/fontawesome-13.1.2.tgz",
|
||||
"integrity": "sha512-Pae4/aDhvSd5FNVy6QOfcQ8uhj+fpbIc2WFDaO9jLEkqM0p5tMZt39Mcfw1XosOXQ0eSqJdlYoI2x8vqbdyzXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-native-vector-icons/common": "^13.0.0"
|
||||
"@react-native-vector-icons/common": "^13.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
@ -4048,12 +4010,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-vector-icons/fontawesome6": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/fontawesome6/-/fontawesome6-13.1.1.tgz",
|
||||
"integrity": "sha512-AwZSCk+2dakqzlBEEKwi/FBc6qg4TtGPPyj2OVt0HcA8sy+gMa0u5iW7hao/Fmq3ad0LQz9HTUYUeslH2jS0jA==",
|
||||
"version": "13.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/fontawesome6/-/fontawesome6-13.1.2.tgz",
|
||||
"integrity": "sha512-oQvQeDE8kSXm3l+oRKQm/Jo4ewR9YdKW2gFDVVl3st1yY5Nml1ZS4m3lTp3a/KehT9w+Uiv2JNn3kG0VOo+AZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-native-vector-icons/common": "^13.0.0"
|
||||
"@react-native-vector-icons/common": "^13.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
@ -4070,12 +4032,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-vector-icons/ionicons": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/ionicons/-/ionicons-13.1.1.tgz",
|
||||
"integrity": "sha512-OAIEf7HW5SnDi+YMRR1W/HBwzWmQiQ4msY8aSQRdVisPvbVFvO6vaWJdV33QI2aj1/5lVLh9oKJGcRsSaBzh2Q==",
|
||||
"version": "13.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/ionicons/-/ionicons-13.1.2.tgz",
|
||||
"integrity": "sha512-8TaXKw41MgKADeesrrbUpA3FR81JNy96ogiGRjWgtE1djSEevDsOKMij7Jq/3TfiGaE0prEshU0TcW5qwsf0Ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-native-vector-icons/common": "^13.0.0"
|
||||
"@react-native-vector-icons/common": "^13.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
@ -4092,12 +4054,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-vector-icons/material-design-icons": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/material-design-icons/-/material-design-icons-13.1.1.tgz",
|
||||
"integrity": "sha512-bKkai9GSMOrqIwKskHZuegejgO6bLp7xNgp7YdeLprkEK44/HsATjCpXhwvRPYq9RSHdOvrFFKBIKLZbkpijSw==",
|
||||
"version": "13.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/material-design-icons/-/material-design-icons-13.1.2.tgz",
|
||||
"integrity": "sha512-Qc8IQCxbnHOk8CvTAb+dLzYgRMbJOLiZ8Up7TRsNixY6EqwPx9/W3DeK5niKtNQ4dIfbALeYz41yyvDM7w7mag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-native-vector-icons/common": "^13.0.0"
|
||||
"@react-native-vector-icons/common": "^13.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
@ -4114,12 +4076,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-vector-icons/material-icons": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/material-icons/-/material-icons-13.1.1.tgz",
|
||||
"integrity": "sha512-u13/5ITff+qGBZBnv3QQ+vLNCNgJzxUfXnMnZDK1rHgpUjH6lex3tSORX5XLYbCuaHDW7WFF0cqzoaephYZApg==",
|
||||
"version": "13.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-vector-icons/material-icons/-/material-icons-13.1.2.tgz",
|
||||
"integrity": "sha512-z8fckMFeYvvVzqfWpsM8AkSFf0pFwlwueKq8/HAKetrZEl3GsK29Mr+sv7me0N6kAl9Z+AaNXqD7gNQpCjkZgg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-native-vector-icons/common": "^13.0.0"
|
||||
"@react-native-vector-icons/common": "^13.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
@ -4752,6 +4714,16 @@
|
||||
"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",
|
||||
@ -4760,9 +4732,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"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==",
|
||||
"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,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0"
|
||||
@ -4801,6 +4774,15 @@
|
||||
"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",
|
||||
@ -4838,12 +4820,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/core": {
|
||||
"version": "7.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.17.4.tgz",
|
||||
"integrity": "sha512-Rv9E2oNNQEkPGpmu9q+vJwGJRSQR6LBg5L+Yo1QHjtwGbHUbjkIKOdYymDZoZYgNzX2OD4rAIlfuzbDKa3cCeA==",
|
||||
"version": "7.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.20.0.tgz",
|
||||
"integrity": "sha512-Lqw5cDQWWxiQnaWv6RhQV95Wr4fh+38/IFVNn1grssyLWV+wXGJjlucXOoU7EVh9jdtcLT8pGyzvsyrvSDywWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^7.5.5",
|
||||
"@react-navigation/routers": "^7.6.0",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"nanoid": "^3.3.11",
|
||||
@ -4863,9 +4845,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-navigation/devtools": {
|
||||
"version": "7.0.58",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/devtools/-/devtools-7.0.58.tgz",
|
||||
"integrity": "sha512-WpADcM0n+QHP1RMMmKZPc4reuvwTyX41gnJCdipjNUG0+VBNOkDyJZpAkeJqOJg2BIjSwsKcTAph3xkmXBjXVA==",
|
||||
"version": "7.0.62",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/devtools/-/devtools-7.0.62.tgz",
|
||||
"integrity": "sha512-Xl+HhZmz0tzJCH13KCs19xYQWPfkQFfYd7Mxv5MnpFdYuxkmvedPJilwAhcTtJc+4PMtQ7sR0Jqv7Ssg4CPblg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@ -4877,18 +4859,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/drawer": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/drawer/-/drawer-7.10.2.tgz",
|
||||
"integrity": "sha512-/ccYFvBPJNzOYioiMQsqjAR4dcQ+7+yjzcuMDTKgsMahLD7Jn7FdOFNtGwMaIQWhfK8KFVMH2KOXAlH/uAGZXw==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-navigation/elements": "^2.9.18",
|
||||
"@react-navigation/elements": "^2.9.23",
|
||||
"color": "^4.2.3",
|
||||
"react-native-drawer-layout": "^4.2.4",
|
||||
"react-native-drawer-layout": "^4.2.5",
|
||||
"use-latest-callback": "^0.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-navigation/native": "^7.2.4",
|
||||
"@react-navigation/native": "^7.3.1",
|
||||
"react": ">= 18.2.0",
|
||||
"react-native": "*",
|
||||
"react-native-gesture-handler": ">= 2.0.0",
|
||||
@ -4898,9 +4880,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/elements": {
|
||||
"version": "2.9.18",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.18.tgz",
|
||||
"integrity": "sha512-mKEvDr6CkCVYZSb8W9WubNseihL+1c8M7ktZJCTCbMk8rQgdQfkdRNwpSUQKspdGpUHCb9cyzvaiuzl1NtjVgw==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
@ -4909,7 +4891,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-native-masked-view/masked-view": ">= 0.2.0",
|
||||
"@react-navigation/native": "^7.2.4",
|
||||
"@react-navigation/native": "^7.3.1",
|
||||
"react": ">= 18.2.0",
|
||||
"react-native": "*",
|
||||
"react-native-safe-area-context": ">= 4.0.0"
|
||||
@ -4921,15 +4903,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/native": {
|
||||
"version": "7.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.4.tgz",
|
||||
"integrity": "sha512-eWC2D3JjhYLId2fVTZhhCiUpWIaPhO9XyEb7Wq8ElmOHyIODlbOzgZ0rKia02OIsDKr9BzZl2sK1dL70yMxDaw==",
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.3.1.tgz",
|
||||
"integrity": "sha512-g1o8jBm87WviR0Eq0wT0M43TSi+uBTz4x8YfHh4XRQ+FHqhNr+uGbuxtGu72QhHtOz0LWnb8UWyvd+M6xWkWHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^7.17.4",
|
||||
"@react-navigation/core": "^7.20.0",
|
||||
"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": {
|
||||
@ -4938,18 +4921,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/native-stack": {
|
||||
"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==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-navigation/elements": "^2.9.18",
|
||||
"@react-navigation/elements": "^2.9.23",
|
||||
"color": "^4.2.3",
|
||||
"sf-symbols-typescript": "^2.1.0",
|
||||
"warn-once": "^0.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-navigation/native": "^7.2.4",
|
||||
"@react-navigation/native": "^7.3.1",
|
||||
"react": ">= 18.2.0",
|
||||
"react-native": "*",
|
||||
"react-native-safe-area-context": ">= 4.0.0",
|
||||
@ -4957,9 +4940,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/routers": {
|
||||
"version": "7.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.5.tgz",
|
||||
"integrity": "sha512-9/hhMte12Kgu+pMnLfA4EWJ0OQmIEAMVMX06FPH2yGkEQSQ3JhhCN/GkcRikzQhtEi97VYYQA15umptBUShcOQ==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.6.0.tgz",
|
||||
"integrity": "sha512-lblhDXfS75jLc7G2K7BZGM+7cjqQXk13X/MA4fq/12r62zM+fBhhreLzYflSitrDDXFRJpSvJXy0ziiGU04Xow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11"
|
||||
@ -5293,11 +5276,6 @@
|
||||
"@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,
|
||||
@ -7985,10 +7963,6 @@
|
||||
"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",
|
||||
@ -8077,9 +8051,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.20",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
|
||||
"integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
|
||||
"version": "1.11.21",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
|
||||
"integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
@ -9833,7 +9807,6 @@
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@ -16311,9 +16284,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-drawer-layout": {
|
||||
"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==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
@ -16466,9 +16439,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-permissions": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-5.5.1.tgz",
|
||||
"integrity": "sha512-nTKFoj47b6EXNqbbg+8VFwBWMpxF1/UTbrNBLpXkWpt005pH4BeFv/NwpcC1iNhToKBrxQD+5kI0z6+kTYoYWA==",
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-5.5.3.tgz",
|
||||
"integrity": "sha512-ngvzzhSC96Wnkz6tslF2BZHJAzBTi1lmrjA4EC/1StAkpNVUssctgotyX+wj/Ti3el/gTCBPOCP3frULMMOepQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
@ -16658,6 +16631,15 @@
|
||||
"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",
|
||||
@ -17526,6 +17508,18 @@
|
||||
"ecpair": "3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/silent-payments/node_modules/@noble/secp256k1": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.6.3.tgz",
|
||||
"integrity": "sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/silent-payments/node_modules/@scure/base": {
|
||||
"version": "1.2.6",
|
||||
"license": "MIT",
|
||||
@ -17896,6 +17890,12 @@
|
||||
"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",
|
||||
@ -18880,9 +18880,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/unicorn-magic": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
|
||||
"integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
|
||||
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
||||
40
package.json
40
package.json
@ -16,19 +16,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@jest/reporters": "^27.5.1",
|
||||
"@react-native/eslint-config": "^0.85.3",
|
||||
"@react-native/jest-preset": "0.85.3",
|
||||
"@react-native/js-polyfills": "^0.85.3",
|
||||
"@react-native/js-polyfills": "^0.86.0",
|
||||
"@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",
|
||||
@ -91,38 +89,39 @@
|
||||
"lint": " npm run tslint && node scripts/find-unused-loc.js && node scripts/find-english-leftovers.js && eslint --ext .js,.ts,.tsx '*.@(js|ts|tsx)' screen 'blue_modules/*.@(js|ts|tsx)' class models loc tests components navigation typings",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"lint:quickfix": "git status --porcelain | grep -v '\\.json' | grep -E '\\.js|\\.ts' --color=never | awk '{print $2}' | xargs eslint --fix; exit 0",
|
||||
"unit": "jest -b -w tests/unit/*"
|
||||
"unit": "jest -b tests/unit/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@arkade-os/boltz-swap": "0.3.37",
|
||||
"@arkade-os/sdk": "0.4.32",
|
||||
"@arkade-os/boltz-swap": "0.3.40",
|
||||
"@arkade-os/sdk": "0.4.35",
|
||||
"@babel/preset-env": "7.29.5",
|
||||
"@bugsnag/react-native": "8.9.0",
|
||||
"@bugsnag/source-maps": "2.3.3",
|
||||
"@keystonehq/bc-ur-registry": "0.7.1",
|
||||
"@ngraveio/bc-ur": "1.1.13",
|
||||
"@noble/hashes": "1.3.3",
|
||||
"@noble/secp256k1": "1.6.3",
|
||||
"@noble/ciphers": "1.3.0",
|
||||
"@noble/hashes": "1.8.0",
|
||||
"@noble/secp256k1": "3.1.0",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-clipboard/clipboard": "1.16.3",
|
||||
"@react-native-community/cli": "20.1.3",
|
||||
"@react-native-community/cli-platform-android": "20.1.3",
|
||||
"@react-native-community/cli-platform-ios": "20.1.3",
|
||||
"@react-native-documents/picker": "12.0.1",
|
||||
"@react-native-vector-icons/entypo": "13.1.1",
|
||||
"@react-native-vector-icons/fontawesome": "13.1.1",
|
||||
"@react-native-vector-icons/fontawesome6": "13.1.1",
|
||||
"@react-native-vector-icons/ionicons": "13.1.1",
|
||||
"@react-native-vector-icons/material-design-icons": "13.1.1",
|
||||
"@react-native-vector-icons/material-icons": "13.1.1",
|
||||
"@react-native-vector-icons/entypo": "13.1.2",
|
||||
"@react-native-vector-icons/fontawesome": "13.1.2",
|
||||
"@react-native-vector-icons/fontawesome6": "13.1.2",
|
||||
"@react-native-vector-icons/ionicons": "13.1.2",
|
||||
"@react-native-vector-icons/material-design-icons": "13.1.2",
|
||||
"@react-native-vector-icons/material-icons": "13.1.2",
|
||||
"@react-native/babel-preset": "0.85.3",
|
||||
"@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.58",
|
||||
"@react-navigation/drawer": "7.10.2",
|
||||
"@react-navigation/native": "7.2.4",
|
||||
"@react-navigation/native-stack": "7.15.1",
|
||||
"@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",
|
||||
"@scure/base": "2.0.0",
|
||||
"@spsina/bip47": "github:BlueWallet/bip47#df82345",
|
||||
"aezeed": "0.0.5",
|
||||
@ -141,8 +140,7 @@
|
||||
"buffer": "6.0.3",
|
||||
"coinselect": "github:BlueWallet/coinselect#35f8038",
|
||||
"crypto-browserify": "3.12.1",
|
||||
"crypto-js": "4.2.0",
|
||||
"dayjs": "1.11.20",
|
||||
"dayjs": "1.11.21",
|
||||
"detox": "20.51.3",
|
||||
"ecpair": "3.0.1",
|
||||
"electrum-client": "github:BlueWallet/rn-electrum-client#83420b8",
|
||||
@ -176,7 +174,7 @@
|
||||
"react-native-linear-gradient": "2.8.3",
|
||||
"react-native-localize": "3.7.0",
|
||||
"react-native-notifications": "5.2.2",
|
||||
"react-native-permissions": "5.5.1",
|
||||
"react-native-permissions": "5.5.3",
|
||||
"react-native-prompt-android": "github:BlueWallet/react-native-prompt-android#ed168d66fed556bc2ed07cf498770f058b78a376",
|
||||
"react-native-quick-actions": "0.3.13",
|
||||
"react-native-reanimated": "4.3.1",
|
||||
|
||||
84
patches/@react-navigation+native-stack+7.15.1.patch
Normal file
84
patches/@react-navigation+native-stack+7.15.1.patch
Normal file
@ -0,0 +1,84 @@
|
||||
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,3 +64,48 @@ 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`).
|
||||
|
||||
12
patches/react-native-screens+4.25.2.patch
Normal file
12
patches/react-native-screens+4.25.2.patch
Normal file
@ -0,0 +1,12 @@
|
||||
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,8 +1,29 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:best-practices",
|
||||
":disableMajorUpdates",
|
||||
":preserveSemverRanges"
|
||||
],
|
||||
"ignoreDeps": ["react-native"]
|
||||
"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"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ const LNDViewInvoice = () => {
|
||||
const [isFetchingInvoices, setIsFetchingInvoices] = useState<boolean>(true);
|
||||
const [invoiceStatusChanged, setInvoiceStatusChanged] = useState<boolean>(false);
|
||||
const [qrCodeSize, setQRCodeSize] = useState<number>(90);
|
||||
const fetchInvoiceInterval = useRef<any>(null);
|
||||
const fetchInvoiceInterval = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
|
||||
const isModal = useNavigationState(state => state.routeNames[0] === LNDCreateInvoice.routeName);
|
||||
|
||||
// Per-swap claim/refund lookup, by the `swap-${id}` prefix mapped onto
|
||||
@ -179,7 +179,6 @@ const LNDViewInvoice = () => {
|
||||
fetchInvoiceInterval.current = setInterval(async () => {
|
||||
if (isFetchingInvoices) {
|
||||
try {
|
||||
// @ts-ignore - getUserInvoices is not set on TWallet
|
||||
const userInvoices: LightningTransaction[] = await wallet.getUserInvoices(20);
|
||||
// fetching only last 20 invoices
|
||||
// for invoice that was created just now - that should be enough (it is basically the last one, so limit=1 would be sufficient)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { RouteProp, StackActions, useIsFocused, useRoute } from '@react-navigation/native';
|
||||
import * as bitcoin from 'bitcoinjs-lib';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ActivityIndicator, ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { ActivityIndicator, ScrollView, StyleSheet, View, TouchableOpacity } from 'react-native';
|
||||
import presentAlert from '../../components/Alert';
|
||||
import { DynamicQRCode } from '../../components/DynamicQRCode';
|
||||
import SaveFileButton from '../../components/SaveFileButton';
|
||||
@ -23,7 +23,7 @@ type RouteParams = RouteProp<SendDetailsStackParamList, 'PsbtMultisigQRCode'>;
|
||||
const PsbtMultisigQRCode: React.FC = () => {
|
||||
const navigation = useExtendedNavigation();
|
||||
const { colors } = useTheme();
|
||||
const openScannerButton = useRef<any>(null);
|
||||
const openScannerButton = useRef<React.ElementRef<typeof TouchableOpacity>>(null);
|
||||
const { params } = useRoute<RouteParams>();
|
||||
const { psbtBase64, isShowOpenScanner, walletID } = params;
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
@ -91,7 +91,7 @@ const SendDetails = () => {
|
||||
const payjoinUrl = route.params?.payjoinUrl;
|
||||
const isTransactionReplaceable = route.params?.isTransactionReplaceable;
|
||||
const routeParams = route.params;
|
||||
const scrollView = useRef<FlatList<any>>(null);
|
||||
const scrollView = useRef<FlatList<IPaymentDestinations>>(null);
|
||||
const scrollIndex = useRef(0);
|
||||
/** Used so we only clear coin-selection (utxos) when the user switches wallet, not on first mount (e.g. Send opened from wallet details with pre-selected UTXOs). */
|
||||
const prevWalletIdForCoinResetRef = useRef<string | null>(null);
|
||||
@ -221,9 +221,6 @@ const SendDetails = () => {
|
||||
}
|
||||
return updatedAddresses;
|
||||
});
|
||||
|
||||
// @ts-ignore: Fix later
|
||||
setParams(prevParams => ({ ...prevParams, addRecipientParams: undefined }));
|
||||
} else {
|
||||
setAddresses([{ address: '', key: String(Math.random()), unit: amountUnit }]); // key is for the FlatList
|
||||
}
|
||||
|
||||
@ -1,22 +1,19 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Linking, StyleSheet, TextInput, View, Pressable, AppState, Text } from 'react-native';
|
||||
import { StyleSheet, View, Pressable, AppState, Text } from 'react-native';
|
||||
import {
|
||||
getDefaultUri,
|
||||
getPushToken,
|
||||
getSavedUri,
|
||||
getStoredNotifications,
|
||||
saveUri,
|
||||
isNotificationsEnabled,
|
||||
setLevels,
|
||||
tryToObtainPermissions,
|
||||
cleanUserOptOutFlag,
|
||||
isGroundControlUriValid,
|
||||
checkPermissions,
|
||||
checkNotificationPermissionStatus,
|
||||
enqueueTestPushNotification,
|
||||
NOTIFICATIONS_NO_AND_DONT_ASK_FLAG,
|
||||
} from '../../blue_modules/notifications';
|
||||
import { BlueSpacing20 } from '../../components/BlueSpacing';
|
||||
import presentAlert from '../../components/Alert';
|
||||
import { BlueSpacing20 } from '../../components/BlueSpacing';
|
||||
import { Button } from '../../components/Button';
|
||||
import CopyToClipboardButton from '../../components/CopyToClipboardButton';
|
||||
import { useTheme } from '../../components/themes';
|
||||
@ -43,7 +40,6 @@ const NotificationSettings: React.FC = () => {
|
||||
const [isNotificationsEnabledState, setNotificationsEnabledState] = useState<boolean | undefined>(undefined);
|
||||
|
||||
const [tokenInfo, setTokenInfo] = useState('<empty>');
|
||||
const [URI, setURI] = useState<string | undefined>();
|
||||
const [tapCount, setTapCount] = useState(0);
|
||||
const { colors } = useTheme();
|
||||
|
||||
@ -139,7 +135,6 @@ const NotificationSettings: React.FC = () => {
|
||||
await updateNotificationStatus();
|
||||
}
|
||||
|
||||
setURI((await getSavedUri()) ?? getDefaultUri());
|
||||
setTokenInfo(
|
||||
'token: ' +
|
||||
JSON.stringify(await getPushToken()) +
|
||||
@ -172,25 +167,17 @@ const NotificationSettings: React.FC = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const save = useCallback(async () => {
|
||||
const enqueueTestPush = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (URI) {
|
||||
if (await isGroundControlUriValid(URI)) {
|
||||
await saveUri(URI);
|
||||
presentAlert({ message: loc.settings.saved });
|
||||
} else {
|
||||
presentAlert({ message: loc.settings.not_a_valid_uri });
|
||||
}
|
||||
} else {
|
||||
await saveUri('');
|
||||
presentAlert({ message: loc.settings.saved });
|
||||
}
|
||||
await enqueueTestPushNotification();
|
||||
} catch (error) {
|
||||
console.error('Error saving URI:', error);
|
||||
console.error('Error enqueueing test push:', error);
|
||||
presentAlert({ message: (error as Error).message });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, [URI]);
|
||||
}, []);
|
||||
|
||||
const renderDeveloperSettings = useCallback(() => {
|
||||
if (tapCount < 10) return null;
|
||||
@ -198,44 +185,9 @@ const NotificationSettings: React.FC = () => {
|
||||
return (
|
||||
<View>
|
||||
<View style={[styles.divider, { backgroundColor: colors.lightBorder ?? colors.borderTopColor }]} />
|
||||
<SettingsCard style={styles.card}>
|
||||
<View style={styles.cardContent}>
|
||||
<Text style={[styles.multilineText, { color: colors.foregroundColor }]}>{loc.settings.groundcontrol_explanation}</Text>
|
||||
</View>
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsListItem
|
||||
title="github.com/BlueWallet/GroundControl"
|
||||
iconName="github"
|
||||
onPress={() => Linking.openURL('https://github.com/BlueWallet/GroundControl')}
|
||||
chevron
|
||||
position="single"
|
||||
spacingTop
|
||||
/>
|
||||
|
||||
<SettingsCard style={styles.card}>
|
||||
<View style={styles.cardContent}>
|
||||
<View
|
||||
style={[
|
||||
styles.uri,
|
||||
{ borderColor: colors.formBorder, borderBottomColor: colors.formBorder, backgroundColor: colors.inputBackgroundColor },
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
placeholder={getDefaultUri()}
|
||||
value={URI}
|
||||
onChangeText={setURI}
|
||||
numberOfLines={1}
|
||||
style={[styles.uriText, { color: colors.alternativeTextColor }]}
|
||||
placeholderTextColor="#81868e"
|
||||
editable={!isLoading}
|
||||
textContentType="URL"
|
||||
autoCapitalize="none"
|
||||
underlineColorAndroid="transparent"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<BlueSpacing20 />
|
||||
<Text style={[styles.centered, { color: colors.foregroundColor }]} onPress={() => setTapCount(tapCount + 1)}>
|
||||
♪ Ground Control to Major Tom ♪
|
||||
</Text>
|
||||
@ -248,12 +200,12 @@ const NotificationSettings: React.FC = () => {
|
||||
</View>
|
||||
|
||||
<BlueSpacing20 />
|
||||
<Button onPress={save} title={loc.settings.save} />
|
||||
<Button onPress={enqueueTestPush} title="Enqueue test push notification" disabled={isLoading} />
|
||||
</View>
|
||||
</SettingsCard>
|
||||
</View>
|
||||
);
|
||||
}, [tapCount, colors, isLoading, URI, tokenInfo, save]);
|
||||
}, [tapCount, colors, isLoading, tokenInfo, enqueueTestPush]);
|
||||
|
||||
const renderPushNotificationsExplanation = useCallback(() => {
|
||||
return (
|
||||
@ -375,28 +327,10 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: horizontalPadding,
|
||||
paddingVertical: isAndroid ? 12 : 10,
|
||||
},
|
||||
multilineText: {
|
||||
lineHeight: 20,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
centered: {
|
||||
textAlign: 'center',
|
||||
marginVertical: 4,
|
||||
},
|
||||
uri: {
|
||||
flexDirection: 'row',
|
||||
borderWidth: 1,
|
||||
borderBottomWidth: 0.5,
|
||||
minHeight: 44,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
},
|
||||
uriText: {
|
||||
flex: 1,
|
||||
marginHorizontal: 8,
|
||||
minHeight: 36,
|
||||
height: 36,
|
||||
},
|
||||
divider: {
|
||||
marginVertical: isAndroid ? 16 : 12,
|
||||
height: 0.5,
|
||||
|
||||
@ -2,7 +2,13 @@ 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 } from '../../components/platform';
|
||||
import {
|
||||
SettingsScrollView,
|
||||
SettingsSection,
|
||||
SettingsListItem,
|
||||
getSettingsHeaderOptions,
|
||||
isIOS26OrHigher,
|
||||
} from '../../components/platform';
|
||||
import { useSettings } from '../../hooks/context/useSettings';
|
||||
import { useTheme } from '../../components/themes';
|
||||
|
||||
@ -15,6 +21,9 @@ 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 { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { NativeStackNavigationOptions, NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import Icon from '../../components/Icon';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
@ -63,6 +63,10 @@ enum ButtonStatus {
|
||||
type RouteProps = RouteProp<DetailViewStackParamList, 'TransactionStatus'>;
|
||||
type NavigationProps = NativeStackNavigationProp<DetailViewStackParamList, 'TransactionStatus'>;
|
||||
|
||||
type TransactionStatusHeaderOptions = NativeStackNavigationOptions & {
|
||||
headerTitleContainerStyle?: { flex: number; maxWidth: number };
|
||||
};
|
||||
|
||||
enum ActionType {
|
||||
SetCPFPPossible,
|
||||
SetRBFBumpFeePossible,
|
||||
@ -136,8 +140,12 @@ type TransactionDetailHeaderTitleProps = {
|
||||
|
||||
const TransactionDetailHeaderTitle: React.FC<TransactionDetailHeaderTitleProps> = ({ direction, date, directionStyle, dateStyle }) => (
|
||||
<View style={styles.headerTitleContainer}>
|
||||
<BlueText style={directionStyle}>{direction}</BlueText>
|
||||
<BlueText style={dateStyle}>{date}</BlueText>
|
||||
<BlueText style={directionStyle} numberOfLines={1} adjustsFontSizeToFit minimumFontScale={0.8}>
|
||||
{direction}
|
||||
</BlueText>
|
||||
<BlueText style={dateStyle} numberOfLines={2} adjustsFontSizeToFit minimumFontScale={0.8}>
|
||||
{date}
|
||||
</BlueText>
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -153,10 +161,57 @@ const TransactionStatus: React.FC = () => {
|
||||
const subscribedWallet = useWalletSubscribe(walletID);
|
||||
const { navigate, goBack, setOptions } = useExtendedNavigation<NavigationProps>();
|
||||
const { colors } = useTheme();
|
||||
const { width: windowWidth } = useWindowDimensions();
|
||||
const { width: windowWidth, fontScale } = 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]);
|
||||
@ -921,15 +976,20 @@ const TransactionStatus: React.FC = () => {
|
||||
<TransactionDetailHeaderTitle
|
||||
direction={transactionDirection}
|
||||
date={transactionDate}
|
||||
directionStyle={[styles.headerTitleDirection, stylesHook.headerTitleDirection]}
|
||||
dateStyle={[styles.headerTitleDate, stylesHook.titleDate]}
|
||||
directionStyle={[styles.headerTitleDirection, stylesHook.headerTitleDirection, scaledStyles.headerTitleDirection]}
|
||||
dateStyle={[styles.headerTitleDate, stylesHook.titleDate, scaledStyles.headerTitleDate]}
|
||||
/>
|
||||
),
|
||||
});
|
||||
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]);
|
||||
}, [tx, transactionDirection, transactionDate, setOptions, colors, windowWidth, scaledStyles]);
|
||||
|
||||
if (loadingError) {
|
||||
return (
|
||||
@ -962,15 +1022,20 @@ const TransactionStatus: React.FC = () => {
|
||||
{/* Value Section */}
|
||||
<View style={styles.valueCard}>
|
||||
<View style={styles.valueContent}>
|
||||
<Text style={[styles.value, stylesHook.value]} selectable numberOfLines={1} adjustsFontSizeToFit minimumFontScale={0.55}>
|
||||
<Text
|
||||
style={[styles.value, stylesHook.value, scaledStyles.value, styles.valueFullWidth]}
|
||||
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]}>
|
||||
<Text style={[styles.localCurrency, stylesHook.localCurrency, scaledStyles.localCurrency]}>
|
||||
{preferredBalanceUnit === BitcoinUnit.LOCAL_CURRENCY
|
||||
? `${formatBalanceWithoutSuffix(Math.abs(txValue), BitcoinUnit.BTC, true)} ${BitcoinUnit.BTC}`
|
||||
: satoshiToLocalCurrency(Math.abs(txValue))}
|
||||
@ -996,8 +1061,10 @@ const TransactionStatus: React.FC = () => {
|
||||
<View style={styles.stateIndicator}>
|
||||
<TransactionPendingIcon />
|
||||
<View style={styles.stateLabelContainer}>
|
||||
<BlueText style={[styles.stateLabel, stylesHook.stateLabelPending]}>{loc.transactions.pending}</BlueText>
|
||||
<BlueText style={[styles.stateValue, stylesHook.stateValuePending, styles.stateValueInline]}>
|
||||
<BlueText style={[styles.stateLabel, stylesHook.stateLabelPending, scaledStyles.stateLabel]}>
|
||||
{loc.transactions.pending}
|
||||
</BlueText>
|
||||
<BlueText style={[styles.stateValue, stylesHook.stateValuePending, styles.stateValueInline, scaledStyles.stateValue]}>
|
||||
{eta || loc.transactions.details_eta_analyzing}
|
||||
</BlueText>
|
||||
</View>
|
||||
@ -1029,9 +1096,11 @@ const TransactionStatus: React.FC = () => {
|
||||
<View style={styles.stateIndicator}>
|
||||
<TransactionOutgoingIcon />
|
||||
<View style={styles.stateLabelContainer}>
|
||||
<BlueText style={[styles.stateLabel, stylesHook.stateLabelSent]}>{loc.transactions.details_sent}</BlueText>
|
||||
<BlueText style={[styles.stateLabel, stylesHook.stateLabelSent, scaledStyles.stateLabel]}>
|
||||
{loc.transactions.details_sent}
|
||||
</BlueText>
|
||||
{isOnChainTx && (
|
||||
<BlueText style={[styles.stateValue, stylesHook.stateValueSent, styles.stateValueInline]}>
|
||||
<BlueText style={[styles.stateValue, stylesHook.stateValueSent, styles.stateValueInline, scaledStyles.stateValue]}>
|
||||
{loc.formatString(loc.transactions.confirmations_lowercase, {
|
||||
confirmations: parsedConfirmations > 6 ? '6+' : parsedConfirmations,
|
||||
})}
|
||||
@ -1043,9 +1112,11 @@ const TransactionStatus: React.FC = () => {
|
||||
<View style={styles.stateIndicator}>
|
||||
<TransactionIncomingIcon />
|
||||
<View style={styles.stateLabelContainer}>
|
||||
<BlueText style={[styles.stateLabel, stylesHook.stateLabelReceived]}>{loc.transactions.details_received}</BlueText>
|
||||
<BlueText style={[styles.stateLabel, stylesHook.stateLabelReceived, scaledStyles.stateLabel]}>
|
||||
{loc.transactions.details_received}
|
||||
</BlueText>
|
||||
{isOnChainTx && (
|
||||
<BlueText style={[styles.stateValue, stylesHook.stateValueReceived, styles.stateValueInline]}>
|
||||
<BlueText style={[styles.stateValue, stylesHook.stateValueReceived, styles.stateValueInline, scaledStyles.stateValue]}>
|
||||
{loc.formatString(loc.transactions.confirmations_lowercase, {
|
||||
confirmations: parsedConfirmations > 6 ? '6+' : parsedConfirmations,
|
||||
})}
|
||||
@ -1080,20 +1151,29 @@ const TransactionStatus: React.FC = () => {
|
||||
{/* Details Section */}
|
||||
<View style={[styles.detailsCard, stylesHook.detailsCard]}>
|
||||
{/* Details Title */}
|
||||
<View style={[styles.sectionTitle, styles.sectionTitleWithButton, stylesHook.sectionTitle]}>
|
||||
<BlueText style={[styles.sectionTitleText, stylesHook.sectionTitleText]}>{loc.transactions.details_section}</BlueText>
|
||||
<View style={[styles.sectionTitle, styles.sectionTitleWithButton, stylesHook.sectionTitle, scaledStyles.sectionTitle]}>
|
||||
<BlueText style={[styles.sectionTitleText, stylesHook.sectionTitleText, styles.sectionTitleTextFlexible]}>
|
||||
{loc.transactions.details_section}
|
||||
</BlueText>
|
||||
{tx?.hash && (
|
||||
<TouchableOpacity
|
||||
onPress={handleOpenBlockExplorer}
|
||||
style={[styles.explorerButton, stylesHook.explorerButton]}
|
||||
style={[styles.explorerButton, stylesHook.explorerButton, scaledStyles.explorerButton]}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<BlueText style={[styles.explorerButtonText, stylesHook.explorerButtonText]}>{loc.transactions.details_explorer}</BlueText>
|
||||
<BlueText
|
||||
style={[styles.explorerButtonText, stylesHook.explorerButtonText]}
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.8}
|
||||
>
|
||||
{loc.transactions.details_explorer}
|
||||
</BlueText>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
{/* Network Fee */}
|
||||
<View style={[styles.detailRow, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_network_fee}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
<CopyTextToClipboard
|
||||
@ -1117,7 +1197,7 @@ const TransactionStatus: React.FC = () => {
|
||||
const displayText = externalAddresses.map(shortenCounterpartyName).join(', ');
|
||||
const copyText = externalAddresses.join(', ');
|
||||
return (
|
||||
<View style={[styles.detailRow, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_to_address}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
<View style={styles.detailValueCopyContainer}>
|
||||
@ -1143,7 +1223,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]}>
|
||||
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_id}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
<View style={styles.detailValueCopyContainer}>
|
||||
@ -1170,7 +1250,7 @@ const TransactionStatus: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Note/Memo */}
|
||||
<View style={[styles.detailRow, styles.detailRowLast, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, styles.detailRowLast, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_note}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
{memo ? (
|
||||
@ -1180,8 +1260,19 @@ const TransactionStatus: React.FC = () => {
|
||||
</BlueText>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity onPress={handleNotePress} style={[styles.addButton, stylesHook.addButton]} activeOpacity={0.7}>
|
||||
<BlueText style={[styles.addButtonText, stylesHook.addButtonText]}>{loc.transactions.details_add_note}</BlueText>
|
||||
<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>
|
||||
)}
|
||||
</View>
|
||||
@ -1192,11 +1283,13 @@ const TransactionStatus: React.FC = () => {
|
||||
<View style={[styles.detailsCard, stylesHook.detailsCard]}>
|
||||
<TouchableOpacity
|
||||
onPress={() => setIsAdvancedExpanded(!isAdvancedExpanded)}
|
||||
style={[styles.advancedHeader, stylesHook.advancedHeader]}
|
||||
style={[styles.advancedHeader, stylesHook.advancedHeader, scaledStyles.advancedHeader]}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<View style={[styles.sectionTitle, stylesHook.sectionTitle, styles.sectionTitleRow]}>
|
||||
<BlueText style={[styles.sectionTitleText, stylesHook.sectionTitleText]}>{loc.transactions.details_advanced}</BlueText>
|
||||
<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>
|
||||
<Icon
|
||||
name={isAdvancedExpanded ? 'chevron-up' : 'chevron-down'}
|
||||
type="font-awesome"
|
||||
@ -1209,7 +1302,7 @@ const TransactionStatus: React.FC = () => {
|
||||
{isAdvancedExpanded && (
|
||||
<View style={[styles.advancedContent, stylesHook.advancedContent]}>
|
||||
{/* Fee Rate */}
|
||||
<View style={[styles.detailRow, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_fee_rate}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
<CopyTextToClipboard
|
||||
@ -1221,7 +1314,7 @@ const TransactionStatus: React.FC = () => {
|
||||
</View>
|
||||
|
||||
{/* Size */}
|
||||
<View style={[styles.detailRow, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_size}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
<CopyTextToClipboard
|
||||
@ -1233,7 +1326,7 @@ const TransactionStatus: React.FC = () => {
|
||||
</View>
|
||||
|
||||
{/* Virtual Size */}
|
||||
<View style={[styles.detailRow, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_virtual_size}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
<CopyTextToClipboard
|
||||
@ -1245,7 +1338,7 @@ const TransactionStatus: React.FC = () => {
|
||||
</View>
|
||||
|
||||
{/* Transaction Hex */}
|
||||
<View style={[styles.detailRow, stylesHook.detailRow]}>
|
||||
<View style={[styles.detailRow, stylesHook.detailRow, scaledStyles.detailRow]}>
|
||||
<BlueText style={[styles.detailLabel, stylesHook.detailLabel]}>{loc.transactions.details_tx_hex}</BlueText>
|
||||
<View style={styles.detailValueContainer}>
|
||||
{txHex ? (
|
||||
@ -1310,6 +1403,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
headerTitleDirection: {
|
||||
fontSize: 17,
|
||||
@ -1357,15 +1451,20 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-start',
|
||||
overflow: 'visible',
|
||||
width: '100%',
|
||||
},
|
||||
value: {
|
||||
fontSize: 40,
|
||||
fontWeight: '700',
|
||||
letterSpacing: -0.5,
|
||||
lineHeight: 32,
|
||||
lineHeight: 48,
|
||||
paddingTop: 8,
|
||||
minHeight: 38,
|
||||
},
|
||||
valueFullWidth: {
|
||||
width: '100%',
|
||||
flexShrink: 1,
|
||||
},
|
||||
valueUnit: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
@ -1383,7 +1482,6 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 12,
|
||||
marginHorizontal: 24,
|
||||
marginBottom: 42,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
stateSection: {
|
||||
alignItems: 'flex-start',
|
||||
@ -1401,6 +1499,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'flex-start',
|
||||
marginLeft: 8,
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
stateLabel: {
|
||||
fontSize: 16,
|
||||
@ -1486,17 +1585,23 @@ 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',
|
||||
minWidth: 50,
|
||||
flexShrink: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
@ -1507,7 +1612,7 @@ const styles = StyleSheet.create({
|
||||
detailRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 0,
|
||||
minHeight: 24,
|
||||
paddingVertical: 12,
|
||||
@ -1531,6 +1636,8 @@ const styles = StyleSheet.create({
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
flex: 1,
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
lineHeight: 22,
|
||||
paddingRight: 12,
|
||||
},
|
||||
@ -1544,11 +1651,12 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
maxWidth: '100%',
|
||||
flexWrap: 'nowrap',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'flex-end',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 8,
|
||||
flexShrink: 0,
|
||||
},
|
||||
detailValueCopyContainer: {
|
||||
flex: 1,
|
||||
@ -1596,7 +1704,7 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 6,
|
||||
alignSelf: 'flex-end',
|
||||
minWidth: 50,
|
||||
flexShrink: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
@ -1614,7 +1722,6 @@ const styles = StyleSheet.create({
|
||||
borderWidth: 1,
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
advancedContent: {
|
||||
marginTop: 0,
|
||||
|
||||
@ -5,7 +5,7 @@ import { StyleSheet, View, ViewStyle, Animated, ScrollView } from 'react-native'
|
||||
import { TWallet } from '../../class/wallets/types';
|
||||
import { Header } from '../../components/Header';
|
||||
import { useTheme } from '../../components/themes';
|
||||
import WalletsCarousel from '../../components/WalletsCarousel';
|
||||
import WalletsCarousel, { CarouselListRefType } from '../../components/WalletsCarousel';
|
||||
import loc from '../../loc';
|
||||
import { useStorage } from '../../hooks/context/useStorage';
|
||||
import TotalWalletsBalance from '../../components/TotalWalletsBalance';
|
||||
@ -94,7 +94,7 @@ const DrawerList: React.FC<DrawerContentComponentProps> = memo((props: DrawerCon
|
||||
const drawerNavigation = props.navigation;
|
||||
|
||||
const [state, dispatch] = useReducer(walletReducer, initialState);
|
||||
const walletsCarousel = useRef<any>(null);
|
||||
const walletsCarousel = useRef<CarouselListRefType>(null);
|
||||
const { wallets, selectedWalletID } = useStorage();
|
||||
const { colors } = useTheme();
|
||||
const isFocused = useIsFocused();
|
||||
|
||||
@ -78,7 +78,7 @@ const ExportMultisigCoordinationSetup: React.FC = () => {
|
||||
const { wallets } = useStorage();
|
||||
const { isPrivacyBlurEnabled } = useSettings();
|
||||
const wallet: TWallet | undefined = wallets.find(w => w.getID() === walletID);
|
||||
const dynamicQRCode = useRef<any>(null);
|
||||
const dynamicQRCode = useRef<DynamicQRCode>(null);
|
||||
const { colors } = useTheme();
|
||||
const { enableScreenProtect, disableScreenProtect } = useScreenProtect();
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import { useTheme } from '../../components/themes';
|
||||
import loc from '../../loc';
|
||||
import { Chain } from '../../models/bitcoinUnits';
|
||||
import { useStorage } from '../../hooks/context/useStorage';
|
||||
import WalletsCarousel from '../../components/WalletsCarousel';
|
||||
import WalletsCarousel, { CarouselListRefType } from '../../components/WalletsCarousel';
|
||||
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
|
||||
import { TWallet } from '../../class/wallets/types';
|
||||
import { pop } from '../../NavigationService';
|
||||
@ -35,7 +35,7 @@ const SelectWallet: React.FC = () => {
|
||||
const { wallets } = useStorage();
|
||||
const { colors } = useTheme();
|
||||
const isModal = useNavigationState(state => state.routes.length > 1);
|
||||
const walletsCarousel = useRef<any>(null);
|
||||
const walletsCarousel = useRef<CarouselListRefType>(null);
|
||||
const previousRouteName = useNavigationState(state => state.routes[state.routes.length - 2]?.name);
|
||||
const [filteredWallets, setFilteredWallets] = useState<TWallet[]>([]);
|
||||
|
||||
|
||||
@ -542,6 +542,7 @@ const WalletDetails: React.FC = () => {
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
testID="WalletNameDisplay"
|
||||
selectable
|
||||
>
|
||||
{walletName}
|
||||
</Text>
|
||||
@ -779,7 +780,6 @@ const WalletDetails: React.FC = () => {
|
||||
containerStyle={stylesHook.listItemContainerBorder}
|
||||
onPress={navigateToXPub}
|
||||
title={loc.wallets.details_show_xpub}
|
||||
chevron
|
||||
testID="XpubButton"
|
||||
bottomDivider
|
||||
/>
|
||||
@ -789,7 +789,6 @@ const WalletDetails: React.FC = () => {
|
||||
containerStyle={stylesHook.listItemContainerBorder}
|
||||
onPress={navigateToSignVerify}
|
||||
title={loc.addresses.sign_title}
|
||||
chevron
|
||||
testID="SignVerify"
|
||||
bottomDivider={!!(wallet.type === MultisigHDWallet.type)}
|
||||
/>
|
||||
@ -840,6 +839,7 @@ const WalletDetails: React.FC = () => {
|
||||
titleStyle={stylesHook.advancedListItemTitle}
|
||||
rightTitle={wallet.typeReadable}
|
||||
rightTitleStyle={stylesHook.advancedListItemRightTitle}
|
||||
rightTitleSelectable
|
||||
bottomDivider={
|
||||
!!(
|
||||
wallet.type === MultisigHDWallet.type ||
|
||||
@ -880,6 +880,7 @@ const WalletDetails: React.FC = () => {
|
||||
isMasterFingerPrintVisible ? (masterFingerprint ?? loc.wallets.import_derivation_loading) : loc.multisig.view
|
||||
}
|
||||
rightTitleStyle={stylesHook.advancedListItemRightTitle}
|
||||
rightTitleSelectable={isMasterFingerPrintVisible}
|
||||
bottomDivider={!!derivationPath}
|
||||
/>
|
||||
)}
|
||||
@ -890,6 +891,7 @@ const WalletDetails: React.FC = () => {
|
||||
titleStyle={stylesHook.advancedListItemTitle}
|
||||
rightTitle={derivationPath}
|
||||
rightTitleStyle={stylesHook.advancedListItemRightTitle}
|
||||
rightTitleSelectable
|
||||
bottomDivider={false}
|
||||
testID="DerivationPath"
|
||||
/>
|
||||
|
||||
@ -11,9 +11,15 @@ 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';
|
||||
@ -27,6 +33,7 @@ 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';
|
||||
@ -35,10 +42,14 @@ import { Chain } from '../../models/bitcoinUnits';
|
||||
import ActionSheet from '../ActionSheet';
|
||||
import { useStorage } from '../../hooks/context/useStorage';
|
||||
import WatchOnlyWarning from '../../components/WatchOnlyWarning';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { NativeStackNavigationOptions, NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
|
||||
import { Transaction, TWallet } from '../../class/wallets/types';
|
||||
import getWalletTransactionsOptions, { WalletTransactionsRouteProps } from '../../navigation/helpers/getWalletTransactionsOptions';
|
||||
import getWalletTransactionsOptions, {
|
||||
WalletTransactionsRouteProps,
|
||||
createWalletDetailsHeaderRight,
|
||||
createWalletDetailsHeaderRightItems,
|
||||
} from '../../navigation/helpers/getWalletTransactionsOptions';
|
||||
import { presentWalletExportReminder } from '../../helpers/presentWalletExportReminder';
|
||||
import selectWallet from '../../helpers/select-wallet';
|
||||
import assert from 'assert';
|
||||
@ -49,6 +60,8 @@ 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
|
||||
@ -59,7 +72,109 @@ type RouteProps = RouteProp<DetailViewStackParamList, 'WalletTransactions'>;
|
||||
|
||||
type WalletTransactionsProps = NativeStackScreenProps<DetailViewStackParamList, 'WalletTransactions'>;
|
||||
|
||||
type TransactionListItem = Transaction & { type: 'transaction' | 'header' };
|
||||
/** 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>;
|
||||
};
|
||||
|
||||
const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { route: WalletTransactionsRouteProps }) => {
|
||||
const { wallets, saveToDisk } = useStorage();
|
||||
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
|
||||
@ -73,8 +188,11 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
const [pageSize] = useState(20);
|
||||
const navigation = useExtendedNavigation();
|
||||
const { setOptions, navigate } = navigation;
|
||||
const { colors } = useTheme();
|
||||
const { colors, dark } = 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);
|
||||
@ -82,12 +200,13 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
const [displayUnit, setDisplayUnit] = useState(wallet.preferredBalanceUnit);
|
||||
const [isUnitSwitching, setIsUnitSwitching] = useState(false);
|
||||
const [isWatchOnlyWarningVisible, setIsWatchOnlyWarningVisible] = useState<boolean>(() => {
|
||||
return wallet.type === WatchOnlyWallet.type && (wallet as any).isWatchOnlyWarningVisible;
|
||||
return wallet.type === WatchOnlyWallet.type && (wallet as WatchOnlyWallet).isWatchOnlyWarningVisible;
|
||||
});
|
||||
const MAX_FAILURES = 3;
|
||||
const flatListRef = useRef<FlatList<Transaction>>(null);
|
||||
const headerRef = useRef<View>(null);
|
||||
const [headerHeight, setHeaderHeight] = useState(0);
|
||||
const headerScrolledRef = useRef(false);
|
||||
const scrolledHeaderOpacity = useSharedValue(0);
|
||||
|
||||
const stylesHook = StyleSheet.create({
|
||||
listHeaderText: {
|
||||
@ -100,44 +219,17 @@ 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' }] },
|
||||
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,
|
||||
},
|
||||
}),
|
||||
sendIcon: {
|
||||
transform: [{ rotate: direction === 'rtl' ? '-225deg' : '225deg' }],
|
||||
},
|
||||
receiveIcon: {
|
||||
transform: [{ rotate: direction === 'rtl' ? '-45deg' : '45deg' }],
|
||||
},
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setOptions(getWalletTransactionsOptions({ route }));
|
||||
}, [route, setOptions]),
|
||||
);
|
||||
|
||||
const onBarCodeRead = useCallback(
|
||||
(ret?: { data?: any }) => {
|
||||
if (!isLoading) {
|
||||
@ -147,9 +239,15 @@ 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);
|
||||
}
|
||||
@ -167,19 +265,14 @@ 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]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsWatchOnlyWarningVisible(wallet.type === WatchOnlyWallet.type && (wallet as any).isWatchOnlyWarningVisible);
|
||||
setIsWatchOnlyWarningVisible(wallet.type === WatchOnlyWallet.type && (wallet as WatchOnlyWallet).isWatchOnlyWarningVisible);
|
||||
// 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);
|
||||
@ -303,7 +396,10 @@ 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -342,11 +438,17 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
[name, navigate, navigation, onWalletSelect, walletID, wallets],
|
||||
);
|
||||
|
||||
const getItemLayout = (_: any, index: number) => ({
|
||||
length: 64,
|
||||
offset: 64 * index,
|
||||
index,
|
||||
});
|
||||
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 renderItem = useCallback(
|
||||
// react/no-unused-prop-types misfires on inline arrow renderers: it reads the
|
||||
@ -391,7 +493,10 @@ 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()) {
|
||||
@ -493,79 +598,163 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [wallet, wallet.hideBalance, displayUnit, balance]);
|
||||
|
||||
const handleScroll = useCallback(
|
||||
(event: any) => {
|
||||
const offsetY = event.nativeEvent.contentOffset.y;
|
||||
const combinedHeight = 180;
|
||||
if (offsetY < combinedHeight) {
|
||||
setOptions({ ...getWalletTransactionsOptions({ route }), headerTitle: undefined });
|
||||
} else {
|
||||
navigation.setOptions({
|
||||
headerTitle: `${wallet.getLabel()} ${walletBalance}`,
|
||||
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;
|
||||
}
|
||||
},
|
||||
[navigation, wallet, walletBalance, setOptions, route],
|
||||
setOptions(getWalletTransactionsOptions({ route }));
|
||||
}, [route, screenWidth, scrolledHeaderTitle, scrolledHeaderOpacity, setOptions]),
|
||||
);
|
||||
|
||||
const measureHeaderHeight = useCallback(() => {
|
||||
if (!headerRef.current) {
|
||||
// If header ref is not available, use default background
|
||||
setHeaderHeight(0);
|
||||
return;
|
||||
}
|
||||
const handleScroll = useCallback(
|
||||
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const offsetY = event.nativeEvent.contentOffset.y;
|
||||
const scrolled = offsetY >= SCROLLED_HEADER_SHOW_OFFSET;
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
const fullHeight = pageY + height;
|
||||
if (fullHeight > 0) {
|
||||
setHeaderHeight(fullHeight);
|
||||
if (scrolled === headerScrolledRef.current) return;
|
||||
headerScrolledRef.current = scrolled;
|
||||
|
||||
if (!scrolled) {
|
||||
setOptions({
|
||||
...getWalletTransactionsOptions({ route }),
|
||||
headerTitle: undefined,
|
||||
headerTitleAlign: undefined,
|
||||
headerTitleContainerStyle: undefined,
|
||||
headerBlurEffect: undefined,
|
||||
});
|
||||
} else {
|
||||
setOptions(getScrolledHeaderOptions());
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
},
|
||||
[getScrolledHeaderOptions, setOptions, route, screenWidth, scrolledHeaderTitle, scrolledHeaderOpacity],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(measureHeaderHeight, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, [walletID, measureHeaderHeight]);
|
||||
|
||||
const ListHeaderComponent = useMemo(
|
||||
const ListHeaderComponent = useCallback(
|
||||
() => (
|
||||
<View ref={headerRef} onLayout={measureHeaderHeight}>
|
||||
<View ref={headerRef}>
|
||||
<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) {
|
||||
wallet.setPreferredBalanceUnit(selectedUnit);
|
||||
} else {
|
||||
(wallet as any).preferredBalanceUnit = selectedUnit;
|
||||
(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 isShouldBeVisible => {
|
||||
const isBiometricsEnabled = await isBiometricUseCapableAndEnabled();
|
||||
if (wallet.hideBalance && isBiometricsEnabled) {
|
||||
const unlocked = await unlockWithBiometrics();
|
||||
if (!unlocked) throw new Error('Biometrics failed');
|
||||
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);
|
||||
}
|
||||
wallet.hideBalance = isShouldBeVisible;
|
||||
await saveToDisk();
|
||||
}}
|
||||
onManageFundsPressed={id => {
|
||||
if (wallet.type === MultisigHDWallet.type) {
|
||||
@ -591,36 +780,30 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<View style={styles.headerBottomBarSpacer}>
|
||||
<View style={stylesHook.headerBottomBar} />
|
||||
<View style={[styles.flex, styles.transactionsSection, stylesHook.backgroundContainer]}>
|
||||
<View style={styles.listHeaderTextRow}>
|
||||
<Text style={[styles.listHeaderText, stylesHook.listHeaderText]}>{loc.transactions.list_title}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={stylesHook.backgroundContainer}>
|
||||
{wallet.type === WatchOnlyWallet.type && isWatchOnlyWarningVisible && (
|
||||
<WatchOnlyWarning
|
||||
handleDismiss={() => {
|
||||
setIsWatchOnlyWarningVisible(false);
|
||||
wallet.isWatchOnlyWarningVisible = false;
|
||||
saveToDisk();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</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 style={stylesHook.backgroundContainer}>
|
||||
{wallet.type === WatchOnlyWallet.type && isWatchOnlyWarningVisible && (
|
||||
<WatchOnlyWarning
|
||||
handleDismiss={() => {
|
||||
setIsWatchOnlyWarningVisible(false);
|
||||
wallet.isWatchOnlyWarningVisible = false;
|
||||
saveToDisk();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
</View>
|
||||
),
|
||||
[
|
||||
wallet,
|
||||
displayUnit,
|
||||
isUnitSwitching,
|
||||
measureHeaderHeight,
|
||||
headerOverlayHeight,
|
||||
stylesHook.backgroundContainer,
|
||||
stylesHook.headerBottomBar,
|
||||
stylesHook.listHeaderText,
|
||||
saveToDisk,
|
||||
isBiometricUseCapableAndEnabled,
|
||||
@ -633,16 +816,18 @@ 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]);
|
||||
}, [walletID, scrolledHeaderOpacity]);
|
||||
|
||||
return (
|
||||
<View style={[styles.flex, stylesHook.backgroundContainer]}>
|
||||
<View style={[styles.refreshIndicatorBackground, stylesHook.gradientBackground]} testID="TransactionsListView" />
|
||||
<View style={[styles.flex, { backgroundColor: WalletGradient.headerColorFor(wallet.type) }]} testID="TransactionsListView">
|
||||
<FlatList<Transaction>
|
||||
ref={flatListRef}
|
||||
style={styles.flatList}
|
||||
getItemLayout={getItemLayout}
|
||||
updateCellsBatchingPeriod={50}
|
||||
onEndReachedThreshold={0.3}
|
||||
@ -653,8 +838,9 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
keyExtractor={_keyExtractor}
|
||||
renderItem={renderItem}
|
||||
initialNumToRender={10}
|
||||
removeClippedSubviews
|
||||
contentContainerStyle={stylesHook.backgroundContainer}
|
||||
removeClippedSubviews={false}
|
||||
contentContainerStyle={[styles.contentContainer, stylesHook.backgroundContainer]}
|
||||
contentInsetAdjustmentBehavior="never"
|
||||
contentInset={{ top: 0, left: 0, bottom: 90, right: 0 }}
|
||||
maxToRenderPerBatch={10}
|
||||
onScroll={handleScroll}
|
||||
@ -671,11 +857,25 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
}
|
||||
refreshControl={
|
||||
!isDesktop && !isElectrumDisabled ? (
|
||||
<RefreshControl refreshing={isLoading} onRefresh={() => refreshTransactions(true)} tintColor={colors.msSuccessCheck} />
|
||||
<RefreshControl
|
||||
refreshing={isLoading}
|
||||
onRefresh={() => refreshTransactions(true)}
|
||||
tintColor={Platform.OS === 'ios' ? 'transparent' : colors.msSuccessCheck}
|
||||
progressViewOffset={headerOverlayHeight}
|
||||
/>
|
||||
) : 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() && (
|
||||
@ -684,7 +884,10 @@ 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 });
|
||||
}
|
||||
@ -735,22 +938,81 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }: { rout
|
||||
|
||||
export default WalletTransactions;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: { flex: 1 },
|
||||
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' },
|
||||
listHeaderText: { marginTop: 0, marginBottom: 16, fontWeight: 'bold', fontSize: 24 },
|
||||
refreshIndicatorBackground: {
|
||||
const scrolledHeaderTitleStyles = StyleSheet.create({
|
||||
animatedTitleWrapper: {
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
iosHeaderRoot: {
|
||||
height: 44,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
iosTitleArea: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 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,
|
||||
},
|
||||
activityIndicator: { marginVertical: 20 },
|
||||
listHeaderTextRow: {
|
||||
flex: 1,
|
||||
marginHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
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,10 +8,15 @@ 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 } from '../../components/FloatButtons';
|
||||
import { FButton, FContainer, FloatButtonsBottomFade, getFloatingButtonReservedHeight } from '../../components/FloatButtons';
|
||||
import { useTheme } from '../../components/themes';
|
||||
import { TransactionListItem } from '../../components/TransactionListItem';
|
||||
import WalletsCarousel, { getWalletCarouselItemWidth } from '../../components/WalletsCarousel';
|
||||
import { TX_ROW_BASE_HEIGHT } from '../../components/ListItem';
|
||||
import WalletsCarousel, {
|
||||
getWalletCarouselItemWidth,
|
||||
CarouselListRefType,
|
||||
getWalletCarouselHeight,
|
||||
} from '../../components/WalletsCarousel';
|
||||
import { useSizeClass, SizeClass } from '../../blue_modules/sizeClass';
|
||||
import loc from '../../loc';
|
||||
import ActionSheet from '../ActionSheet';
|
||||
@ -25,8 +30,10 @@ 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;
|
||||
@ -101,13 +108,17 @@ const WalletsList: React.FC = () => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const { isLoading } = state;
|
||||
const { sizeClass, isLarge } = useSizeClass();
|
||||
const walletsCarousel = useRef<any>(null);
|
||||
const walletsCarousel = useRef<CarouselListRefType>(null);
|
||||
const connectionPoll = useContext(ConnectionPollContext);
|
||||
const currentWalletIndex = useRef<number>(0);
|
||||
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
|
||||
const { wallets, getTransactions, refreshAllWalletTransactions } = useStorage();
|
||||
const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings();
|
||||
const { width } = useWindowDimensions();
|
||||
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 { colors, scanImage } = useTheme();
|
||||
const navigation = useExtendedNavigation<NavigationProps>();
|
||||
const isFocused = useIsFocused();
|
||||
@ -123,9 +134,11 @@ 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),
|
||||
},
|
||||
});
|
||||
|
||||
@ -471,7 +484,9 @@ const WalletsList: React.FC = () => {
|
||||
|
||||
const sectionListKeyExtractor = useCallback((item: any, index: any) => {
|
||||
if (typeof item === 'string') return item;
|
||||
return item?.hash || item?.txid || `${item}${index}`;
|
||||
const txKey = item?.hash || item?.txid;
|
||||
if (txKey && item?.walletID) return `${txKey}_${item.walletID}`;
|
||||
return txKey || `${item}${index}`;
|
||||
}, []);
|
||||
|
||||
const refreshProps = isDesktop || isElectrumDisabled ? {} : { refreshing: isLoading, onRefresh };
|
||||
@ -490,14 +505,9 @@ 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 SECTION_HEADER_HEIGHT + (sizeClass === SizeClass.Large ? LARGE_TITLE_EXTRA_HEIGHT : 0);
|
||||
}, [sizeClass]);
|
||||
return sectionHeaderHeight + (sizeClass === SizeClass.Large ? Math.round(20 * fontScale) : 0);
|
||||
}, [sizeClass, sectionHeaderHeight, fontScale]);
|
||||
|
||||
const getItemLayout = useCallback(
|
||||
(data: any, index: number) => {
|
||||
@ -506,8 +516,8 @@ const WalletsList: React.FC = () => {
|
||||
if (sizeClass === SizeClass.Large) {
|
||||
// On large screens: only transaction items, no carousel
|
||||
return {
|
||||
length: TRANSACTION_ITEM_HEIGHT,
|
||||
offset: TRANSACTION_ITEM_HEIGHT * index,
|
||||
length: transactionItemHeight,
|
||||
offset: transactionItemHeight * index,
|
||||
index,
|
||||
};
|
||||
} else {
|
||||
@ -515,7 +525,7 @@ const WalletsList: React.FC = () => {
|
||||
// First section: Carousel
|
||||
if (index === 0) {
|
||||
return {
|
||||
length: CAROUSEL_HEIGHT,
|
||||
length: carouselHeight,
|
||||
offset: 0,
|
||||
index,
|
||||
};
|
||||
@ -528,13 +538,13 @@ const WalletsList: React.FC = () => {
|
||||
// 3. Transaction items
|
||||
const transactionIndex = index - 1; // Adjust index to account for carousel
|
||||
return {
|
||||
length: TRANSACTION_ITEM_HEIGHT,
|
||||
offset: CAROUSEL_HEIGHT + headerHeight + TRANSACTION_ITEM_HEIGHT * transactionIndex,
|
||||
length: transactionItemHeight,
|
||||
offset: carouselHeight + headerHeight + transactionItemHeight * transactionIndex,
|
||||
index,
|
||||
};
|
||||
}
|
||||
},
|
||||
[sizeClass, getSectionHeaderHeight],
|
||||
[sizeClass, getSectionHeaderHeight, carouselHeight, transactionItemHeight],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -547,11 +557,13 @@ const WalletsList: React.FC = () => {
|
||||
initialNumToRender={10}
|
||||
renderSectionFooter={renderSectionFooter}
|
||||
sections={sections}
|
||||
floatingButtonHeight={70}
|
||||
floatingButtonHeight={floatingButtonHeight}
|
||||
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,6 +4,7 @@ import { element, waitFor } from 'detox';
|
||||
|
||||
import {
|
||||
confirmPasswordDialog,
|
||||
dismissAlertByText,
|
||||
expectToBeVisible,
|
||||
extractTextFromElementById,
|
||||
goBack,
|
||||
@ -193,31 +194,36 @@ describe('BlueWallet UI Tests - no wallets', () => {
|
||||
await waitFor(element(by.id('NotificationsSwitch')))
|
||||
.toBeVisible()
|
||||
.withTimeout(10000);
|
||||
await element(by.id('NotificationsSwitch')).tap();
|
||||
|
||||
// If notifications are not enabled on the device, an alert will appear
|
||||
// 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;
|
||||
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
|
||||
}
|
||||
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
|
||||
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 goBack();
|
||||
await goBack();
|
||||
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();
|
||||
}
|
||||
} else {
|
||||
await goBack();
|
||||
}
|
||||
|
||||
@ -58,8 +58,20 @@ export async function waitForText(text, timeout = 33000) {
|
||||
await waitFor(element(by.text(text)))
|
||||
.toBeVisible()
|
||||
.withTimeout(timeout / 2);
|
||||
return true;
|
||||
} catch (err) {
|
||||
rethrowWithCallsite(err, callsite);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,7 +185,7 @@ export async function helperDeleteWallet(label, remainingBalanceSat = false) {
|
||||
await waitForId('WalletDetails');
|
||||
await element(by.id('WalletDetails')).tap();
|
||||
await element(by.id('WalletDetailsScroll')).swipe('up', 'fast', 1);
|
||||
await sleep(200);
|
||||
await sleep(1000);
|
||||
await element(by.id('DeleteWallet')).tap();
|
||||
await waitForText('Yes, delete');
|
||||
await element(by.text('Yes, delete')).tap();
|
||||
@ -216,15 +228,44 @@ 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');
|
||||
|
||||
await waitFor(element(by.id('PleasebackupOk')))
|
||||
.toBeVisible()
|
||||
.whileElement(by.id('PleaseBackupScrollView'))
|
||||
.scroll(500, 'down'); // in case emu screen is small and it doesnt fit
|
||||
// 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 element(by.id('PleasebackupOk')).tap();
|
||||
await scrollUpOnHomeScreen();
|
||||
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 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);
|
||||
@ -297,6 +338,46 @@ 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.
|
||||
@ -368,19 +449,62 @@ export async function goBack() {
|
||||
// Try each back/close affordance in order; retry the full set up to 10 times.
|
||||
const candidates = [by.id('BackButton'), by.id('NavigationCloseButton'), by.label('Back'), by.text('Close')];
|
||||
|
||||
// A matcher can hit several elements across stacked screens: each nav back
|
||||
// button exists twice (_UIButtonBarButton wrapper + UIAccessibilityBackButtonElement),
|
||||
// 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) {
|
||||
try {
|
||||
await element(matcher).atIndex(0).tap();
|
||||
return;
|
||||
} catch (_) {
|
||||
/* try next */
|
||||
for (let idx = 0; idx < 6; idx++) {
|
||||
let attrs;
|
||||
try {
|
||||
attrs = await element(matcher).atIndex(idx).getAttributes();
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
break; // no element at this index — try next candidate
|
||||
}
|
||||
if (!attrs.visible || attrs.hittable === false) continue;
|
||||
try {
|
||||
await element(matcher).atIndex(idx).tap();
|
||||
return;
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
rethrowWithCallsite(new Error('goBack: no back/close affordance tappable after 10 attempts.'), callsite);
|
||||
const wrapped = new Error('goBack: no back/close affordance tappable after 10 attempts.');
|
||||
if (lastErr) wrapped.cause = lastErr;
|
||||
rethrowWithCallsite(wrapped, callsite);
|
||||
}
|
||||
|
||||
export async function typeTextIntoAlertInput(text) {
|
||||
@ -405,7 +529,7 @@ export async function scrollUpOnHomeScreen() {
|
||||
// if no wallets there will be just one scroll
|
||||
await element(by.type('RCTEnhancedScrollView')).swipe('down', 'slow', 0.5);
|
||||
}
|
||||
await sleep(200); // bounce animation
|
||||
await sleep(1000); // bounce animation
|
||||
}
|
||||
|
||||
// We really only need this function when running tests locally.
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
*/
|
||||
|
||||
import { closeAllArkadeRealms, __testing__ as realmTesting } from '../../blue_modules/arkade-adapters/realm/realmInstance';
|
||||
import { __testing__ as walletTesting } from '../../class/wallets/lightning-ark-wallet';
|
||||
import { LightningArkWallet, __testing__ as walletTesting } from '../../class/wallets/lightning-ark-wallet';
|
||||
|
||||
const Realm = require('realm');
|
||||
|
||||
@ -81,3 +81,39 @@ export const arkadeMockState = {
|
||||
Keychain.__mockKeychainHelpers.store.set(service, { username: service, password, service });
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Tear down a LightningArkWallet after integration tests. Stops SDK background
|
||||
* loops (ContractWatcher SSE, VtxoManager polling, SwapManager) via dispose()
|
||||
* before clearing module-private caches.
|
||||
*/
|
||||
export async function teardownArkadeWallet(w: LightningArkWallet): Promise<void> {
|
||||
try {
|
||||
await w.onDelete();
|
||||
} catch {
|
||||
// onDelete already logs and swallows per-namespace errors.
|
||||
}
|
||||
}
|
||||
|
||||
/** Best-effort dispose of any Arkade SDK runtime still cached module-wide. */
|
||||
export async function disposeAllArkadeRuntime(): Promise<void> {
|
||||
for (const ns of Object.keys(walletTesting.staticSwapsCache)) {
|
||||
const swaps = walletTesting.staticSwapsCache[ns];
|
||||
try {
|
||||
if (typeof swaps?.dispose === 'function') await swaps.dispose();
|
||||
} catch {}
|
||||
delete walletTesting.staticSwapsCache[ns];
|
||||
}
|
||||
for (const ns of Object.keys(walletTesting.staticWalletCache)) {
|
||||
const sdkWallet = walletTesting.staticWalletCache[ns];
|
||||
try {
|
||||
if (typeof sdkWallet?.dispose === 'function') await sdkWallet.dispose();
|
||||
} catch {}
|
||||
delete walletTesting.staticWalletCache[ns];
|
||||
}
|
||||
walletTesting.initInFlight.clear();
|
||||
walletTesting.restoreInFlight.clear();
|
||||
for (const k of Object.keys(walletTesting.boardingLock)) delete walletTesting.boardingLock[k];
|
||||
closeAllArkadeRealms();
|
||||
realmTesting.openInFlight.clear();
|
||||
}
|
||||
|
||||
@ -110,3 +110,25 @@ export function installSdkProviderSpies(): void {
|
||||
export function restoreSdkProviderSpies(): void {
|
||||
jest.restoreAllMocks();
|
||||
}
|
||||
|
||||
let backgroundLoopSpies: jest.SpiedFunction<any>[] = [];
|
||||
|
||||
export function restoreSdkBackgroundLoopStubs(): void {
|
||||
for (const spy of backgroundLoopSpies) spy.mockRestore();
|
||||
backgroundLoopSpies = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub only the SDK background subscriptions that Jest cannot shut down
|
||||
* cleanly (VtxoManager polling, SwapManager WebSocket, ContractWatcher SSE).
|
||||
* Real HTTP calls (getInfo, getTransactionHistory, restoreSwaps, etc.) still
|
||||
* run — use in env-gated integration tests that hit production services.
|
||||
*/
|
||||
export function installSdkBackgroundLoopStubs(): void {
|
||||
restoreSdkBackgroundLoopStubs();
|
||||
backgroundLoopSpies = [
|
||||
jest.spyOn(VtxoManager.prototype as any, 'initializeSubscription').mockResolvedValue(undefined),
|
||||
jest.spyOn(SwapManager.prototype as any, 'start').mockResolvedValue(undefined),
|
||||
jest.spyOn(ContractManager.prototype as any, 'initialize').mockResolvedValue(undefined),
|
||||
];
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ import assert from 'assert';
|
||||
|
||||
import { HDSegwitBech32Wallet } from '../../class/wallets/hd-segwit-bech32-wallet';
|
||||
import { LightningArkWallet } from '../../class/wallets/lightning-ark-wallet.ts';
|
||||
import { disposeAllArkadeRuntime, teardownArkadeWallet } from '../helpers/arkadeMocks';
|
||||
import { installSdkBackgroundLoopStubs, restoreSdkBackgroundLoopStubs } from '../helpers/sdkProviderMocks';
|
||||
|
||||
// Ark storage lives in Realm, not AsyncStorage. Realm + Keychain are mocked
|
||||
// globally by tests/setup.js (per-path Realm + service-keyed Keychain), and
|
||||
@ -15,29 +17,41 @@ jest.setTimeout(30_000);
|
||||
const w = new LightningArkWallet();
|
||||
|
||||
beforeAll(async () => {
|
||||
// Install before the env guard: `can generate` runs init() regardless of
|
||||
// HD_MNEMONIC_OLD, and without the stubs its background loops keep Jest alive.
|
||||
installSdkBackgroundLoopStubs();
|
||||
if (!process.env.HD_MNEMONIC_OLD) {
|
||||
console.error('process.env.HD_MNEMONIC_OLD not set, skipped');
|
||||
return;
|
||||
}
|
||||
w.setSecret('arkade://' + process.env.HD_MNEMONIC_OLD);
|
||||
await w.init();
|
||||
await w.restoreSwaps();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 3_000)); // sleep
|
||||
if (process.env.HD_MNEMONIC_OLD) {
|
||||
await teardownArkadeWallet(w);
|
||||
}
|
||||
await disposeAllArkadeRuntime();
|
||||
restoreSdkBackgroundLoopStubs();
|
||||
});
|
||||
|
||||
describe('LightningArkWallet (integration)', () => {
|
||||
it('can generate', async () => {
|
||||
const wGenerated = new LightningArkWallet();
|
||||
await wGenerated.generate();
|
||||
try {
|
||||
await wGenerated.generate();
|
||||
|
||||
assert.ok(wGenerated.getSecret().startsWith('arkade://'));
|
||||
assert.ok(wGenerated.getSecret().startsWith('arkade://'));
|
||||
|
||||
const mnemonics = wGenerated.getSecret().replace('arkade://', '');
|
||||
const hd = new HDSegwitBech32Wallet();
|
||||
hd.setSecret(mnemonics);
|
||||
assert.ok(hd.validateMnemonic());
|
||||
const mnemonics = wGenerated.getSecret().replace('arkade://', '');
|
||||
const hd = new HDSegwitBech32Wallet();
|
||||
hd.setSecret(mnemonics);
|
||||
assert.ok(hd.validateMnemonic());
|
||||
} finally {
|
||||
await teardownArkadeWallet(wGenerated);
|
||||
}
|
||||
});
|
||||
|
||||
it('can fetch balance', async () => {
|
||||
@ -70,47 +84,48 @@ describe('LightningArkWallet (integration)', () => {
|
||||
}
|
||||
|
||||
await w.fetchTransactions();
|
||||
await w.fetchUserInvoices();
|
||||
|
||||
const txs = w.getTransactions();
|
||||
assert.ok(txs.length > 0);
|
||||
assert.ok(txs.length > 0, 'Should have transaction history from the Ark indexer');
|
||||
|
||||
// Find the reverse swap (incoming) transaction
|
||||
const receiveTx = txs.find(t => t.value! > 0);
|
||||
assert.ok(receiveTx, 'Should have at least one receive transaction');
|
||||
assert.strictEqual(receiveTx.memo, 'test invoice');
|
||||
assert.strictEqual(receiveTx.value, 9999);
|
||||
assert.strictEqual(receiveTx.timestamp, 1761224952);
|
||||
assert.strictEqual(receiveTx.ispaid, true);
|
||||
assert.ok(receiveTx.payment_hash);
|
||||
assert.ok(receiveTx.payment_request);
|
||||
assert.strictEqual(receiveTx.payment_preimage, '7244f7e956a91171038ea935d56cdb758cc36c345f0aa92764bfed6fe6fc9b17');
|
||||
assert.ok(receiveTx.value! > 0);
|
||||
assert.ok(receiveTx.timestamp! > 0);
|
||||
assert.ok(receiveTx.memo);
|
||||
|
||||
// Find the submarine swap (outgoing) transaction
|
||||
const sendTx = txs.find(t => t.value! < 0);
|
||||
assert.ok(sendTx, 'Should have at least one send transaction');
|
||||
assert.strictEqual(sendTx.value, -8001);
|
||||
assert.strictEqual(sendTx.timestamp, 1761225645);
|
||||
assert.strictEqual(sendTx.ispaid, true);
|
||||
assert.ok(sendTx.payment_hash);
|
||||
assert.ok(sendTx.payment_request);
|
||||
assert.strictEqual(sendTx.payment_preimage, '182fb8f273bda01b22c0e91991e093e18b2970f389fc7f7a2121870324eb2de5');
|
||||
const swapHistory: any[] = (w as any)._swapHistory ?? [];
|
||||
const settledReverse = swapHistory.find(s => s.type === 'reverse' && s.status === 'invoice.settled');
|
||||
if (settledReverse) {
|
||||
// When Boltz reverse-swap history is restored, settled receives are enriched in place.
|
||||
assert.strictEqual(receiveTx.ispaid, true);
|
||||
assert.ok(receiveTx.payment_hash);
|
||||
assert.ok(receiveTx.payment_request);
|
||||
assert.ok(receiveTx.payment_preimage);
|
||||
assert.notStrictEqual(receiveTx.memo, 'Received');
|
||||
|
||||
const ownInvoice = settledReverse.request?.invoice || settledReverse.response?.invoice;
|
||||
if (ownInvoice) {
|
||||
assert.ok(w.isInvoiceGeneratedByWallet(ownInvoice));
|
||||
}
|
||||
}
|
||||
|
||||
const settledSubmarine = swapHistory.find(s => s.type === 'submarine' && s.status === 'transaction.claimed');
|
||||
if (settledSubmarine) {
|
||||
const sendTx = txs.find(t => t.value! < 0);
|
||||
assert.ok(sendTx, 'Should have a send transaction when submarine swap history exists');
|
||||
assert.strictEqual(sendTx.ispaid, true);
|
||||
assert.ok(sendTx.payment_hash);
|
||||
assert.ok(sendTx.payment_request);
|
||||
assert.ok(sendTx.payment_preimage);
|
||||
}
|
||||
|
||||
const invoices = await w.getUserInvoices();
|
||||
assert.ok(invoices.length > 0);
|
||||
assert(invoices[0].value! > 0);
|
||||
assert(invoices[0].ispaid);
|
||||
|
||||
assert.ok(
|
||||
w.isInvoiceGeneratedByWallet(
|
||||
'lnbc100u1p50528cpp5rhy4fgs0ff23asecxtxt9zvc3apn0p8h7fxsj0d5k7j3x92zwhlqdq5w3jhxapqd9h8vmmfvdjscqrp80xqyf8ucsp5vcsrzye432n9wh0zwuv5z8y5n9zvkwpctr685e80utzc2yueccms9qxpqysgqd87swq3hput9k6llp0wxg098hc7ge3e5nrtnvak6zreywzaf4k9s8d3u4hrmt3m22kf0jt7ruqj0caknk5ykzdenjdphz50t7xrstnqqn6aw0m',
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
!w.isInvoiceGeneratedByWallet(
|
||||
'lnbc80u1p5052hwpp5z4ln6hyq4wcck809pt7f0q54ag5he6ce797flm7gl9vuccm9lx2sdqqcqzysxqyz5vqsp5nh9fl4g36606tvxswtnfxzy55yze2656cw2fya7dhl8r6u0czyds9qxpqysgq83sw25g9d9ltr05nkfzejnvvunzkrk4qeuxhszuvvsguk5m6vmg3a7n5nd67l9frru3kjzpt8x6jfusjyc7ezh49jeeh900kt3v30qsqzq7fst',
|
||||
),
|
||||
);
|
||||
if (settledReverse) {
|
||||
assert.ok(invoices.length > 0);
|
||||
assert(invoices[0].value! > 0);
|
||||
assert(invoices[0].ispaid);
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
import assert from 'assert';
|
||||
import { isGroundControlUriValid } from '../../blue_modules/notifications';
|
||||
|
||||
// Notifications.default = new Notifications();
|
||||
|
||||
describe('notifications', () => {
|
||||
// yeah, lets rely less on external services...
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('can check groundcontrol server uri validity', async () => {
|
||||
assert.ok(await isGroundControlUriValid('https://groundcontrol.bluewallet.io/'));
|
||||
assert.ok(!(await isGroundControlUriValid('https://www.google.com')));
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
});
|
||||
|
||||
// muted because it causes jest to hang waiting indefinitely
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('can check non-responding url', async () => {
|
||||
assert.ok(!(await isGroundControlUriValid('https://localhost.com')));
|
||||
});
|
||||
});
|
||||
@ -7,7 +7,8 @@ console.warn = (...args) => {
|
||||
if (
|
||||
typeof args[0] === 'string' &&
|
||||
(args[0].startsWith('WARNING: Sending to a future segwit version address can lead to loss of funds') ||
|
||||
args[0].startsWith('only compressed public keys are good'))
|
||||
args[0].startsWith('only compressed public keys are good') ||
|
||||
args[0].startsWith('Using standard fetch instead of expo/fetch'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@ -510,6 +511,17 @@ jest.mock('../blue_modules/analytics', () => {
|
||||
return ret;
|
||||
});
|
||||
|
||||
// addInvoice() registers a fire-and-forget payment-push callback; disable the
|
||||
// URI in unit tests so node-fetch does not leave in-flight handles after the
|
||||
// suite exits (which makes Jest fail with "did not exit one second after").
|
||||
jest.mock('../blue_modules/constants', () => {
|
||||
const actual = jest.requireActual('../blue_modules/constants');
|
||||
return {
|
||||
...actual,
|
||||
arkadePaymentPushUri: '',
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('react-native-share', () => {
|
||||
return {
|
||||
open: jest.fn(),
|
||||
|
||||
@ -43,4 +43,19 @@ 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');
|
||||
});
|
||||
});
|
||||
|
||||
51
tests/unit/evp-bytes-to-key.test.ts
Normal file
51
tests/unit/evp-bytes-to-key.test.ts
Normal file
@ -0,0 +1,51 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
10
tests/unit/fixtures/unchained.json
Normal file
10
tests/unit/fixtures/unchained.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"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,6 +1160,7 @@ 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);
|
||||
@ -1167,6 +1168,11 @@ 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,6 +208,17 @@ 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,6 +2161,31 @@ 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());
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react-native';
|
||||
|
||||
import { _setSkipUpdateExchangeRate } from '../../blue_modules/currency';
|
||||
import TransactionStatus from '../../screen/transactions/TransactionStatus';
|
||||
|
||||
// TransactionStatus renders fiat amounts via satoshiToLocalCurrency(), which
|
||||
// kicks off a real exchange-rate fetch when no rate is cached — leaving a TLS
|
||||
// socket open after the run ("Jest did not exit one second after...").
|
||||
_setSkipUpdateExchangeRate();
|
||||
|
||||
type MockStorage = {
|
||||
wallets: any[];
|
||||
txMetadata: Record<string, any>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user