Compare commits

..

35 Commits

Author SHA1 Message Date
GLaDOS
5007f88085
Merge pull request #8724 from BlueWallet/fix-fastlane-android
fix: fastlate android
2026-07-02 10:15:27 +01:00
GLaDOS
6e783a1444
Merge pull request #8714 from BlueWallet/new-lang-ak
ADD: Akan (ak) language
2026-07-02 06:28:57 +01:00
Ivan Vershigora
f86e560de4
fix: fastlate android 2026-07-02 00:01:45 +01:00
Ivan Vershigora
7ea7fa7949
ADD: Akan (ak) language
Add full Akan translation (loc/ak.json), a per-language translation
vocabulary (loc/vocabulary/ak.md), and wire the language into the picker
(loc/languages.ts) and loader + dayjs locale (loc/index.ts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:52:27 +01:00
GLaDOS
d2dfb1465b
Merge pull request #8704 from BlueWallet/new-lang-hy
ADD: Armenian (hy) language
2026-07-01 21:11:31 +01:00
Ivan
ce8c298d64
fix: sync language files (#8703)
* fix: sync language files

* fix: sync language files

* FIX: sync Faroese (fo) from Transifex + vocabulary corrections

Re-sync loc/fo.json FROM Transifex to pick up the maintainer's corrections
(81 strings) instead of overwriting them; the two still-untranslated new keys
(wallets.restore_swap_activity[_done]) are omitted so they fall back to en.
Update loc/vocabulary/fo.md per maintainer feedback (myntval/QR-kota
hyphenation, Speed Up→skunda undir, Frozen→læstur, Settings vs Options=kostur,
Input/Output singular↔plural note, details_explorer ambiguity). Whitelist the
maintainer's intentional English "Send" label in find-english-leftovers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:11:01 +01:00
Ivan Vershigora
9d84def082
ADD: Armenian (hy) language
Add full Armenian translation (loc/hy.json), a per-language translation
vocabulary (loc/vocabulary/hy.md), and wire the language into the picker
(loc/languages.ts) and loader + dayjs locale (loc/index.ts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:31:49 +01:00
GLaDOS
f00eabe2b3
Merge pull request #8709 from BlueWallet/new-lang-fil_PH
ADD: Filipino (fil_PH) language
2026-07-01 12:07:04 +01:00
GLaDOS
193d9cfe94
Merge pull request #8723 from BlueWallet/fix-fastlane
FIX: repair Transifex fastlane metadata integration
2026-07-01 12:06:40 +01:00
Ivan Vershigora
1cf6289f76 FIX: remove stray </content> tag from fil_PH vocabulary
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:51:38 +01:00
Ivan Vershigora
cd4423c936 ADD: Filipino (fil_PH) language
Add full Filipino translation (loc/fil_PH.json), a per-language translation
vocabulary (loc/vocabulary/fil_PH.md), and wire the language into the picker
(loc/languages.ts) and loader + dayjs locale (loc/index.ts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:51:38 +01:00
Carlos Oliveira
57f5ec5efd FIX: keep signed transaction hex screen scrollable 2026-06-30 17:40:13 +01:00
Overtorment
0cec7f6909
Merge branch 'master' into fix-fastlane 2026-06-30 17:38:13 +01:00
Copilot
d437726e6a FIX: trust wix tap before applesimutils install (#8727) 2026-06-30 17:37:39 +01:00
Ivan Vershigora
0549a2e6e2 fix: ark tests 2026-06-30 17:37:39 +01:00
Ivan Vershigora
c12b758c42
FIX: repair Transifex fastlane metadata integration 2026-06-29 17:29:43 +01:00
GLaDOS
32d3f77f4f
Merge pull request #8694 from BlueWallet/renovate/rubygems-concurrent-ruby-vulnerability
Some checks failed
BuildReleaseApk / buildReleaseApk (push) Has been cancelled
Build Release and Upload to TestFlight (iOS) / build (push) Has been cancelled
Build Release and Upload to TestFlight (iOS) / testflight-upload (push) Has been cancelled
BuildReleaseApk / browserstack (push) Has been cancelled
OPS: Update dependency concurrent-ruby to '< 1.3.8' [SECURITY]
2026-06-22 17:13:35 +01:00
GLaDOS
f26ff9189c
Merge pull request #8695 from BlueWallet/fix-walletcarouselclipping
FIX: clipping wallet balance on carousel
2026-06-22 17:13:29 +01:00
ncoelho
1fa290652c FIX: clipping wallet balance on carousel 2026-06-22 15:41:14 +02:00
renovate[bot]
099f6f46a6
OPS: Update dependency concurrent-ruby to '< 1.3.8' [SECURITY] 2026-06-22 13:32:53 +00:00
Nuno
01a11bc8dd
FIX: text size on main app views (#8689)
* fix: text size on wallet view

* fix big font sizes

* fix lint

* fix Glados comments

* fix: run prettier

---------

Co-authored-by: Ivan Vershigora <ivan.vershigora@gmail.com>
2026-06-22 15:27:48 +02:00
GLaDOS
6639891c24
Merge pull request #8632 from BlueWallet/fix-custom-input-lag
Some checks failed
Build Release and Upload to TestFlight (iOS) / build (push) Has been cancelled
BuildReleaseApk / buildReleaseApk (push) Has been cancelled
Build Release and Upload to TestFlight (iOS) / testflight-upload (push) Has been cancelled
BuildReleaseApk / browserstack (push) Has been cancelled
FIX: Amount input lag in rbf custom fee input
2026-06-20 11:45:47 +01:00
GLaDOS
4029d294f8
Merge pull request #8566 from BlueWallet/cryptojs
Some checks are pending
Build Release and Upload to TestFlight (iOS) / build (push) Waiting to run
Build Release and Upload to TestFlight (iOS) / testflight-upload (push) Blocked by required conditions
BuildReleaseApk / buildReleaseApk (push) Waiting to run
BuildReleaseApk / browserstack (push) Blocked by required conditions
REF: swap crypto-js for @noble/ciphers + hashes
2026-06-19 15:00:49 +01:00
Ivan Vershigora
276a9ea8f8
REF: swap crypto-js for @noble/ciphers + hashes 2026-06-19 12:23:35 +01:00
Nuno
d415f1a0b8
feat: iOS 26 glass (#8508)
Some checks failed
Build Release and Upload to TestFlight (iOS) / build (push) Has been cancelled
BuildReleaseApk / buildReleaseApk (push) Has been cancelled
Build Release and Upload to TestFlight (iOS) / testflight-upload (push) Has been cancelled
BuildReleaseApk / browserstack (push) Has been cancelled
2026-06-18 16:37:24 +01:00
Nuno
6124cf1c04
fix: key on tx list (#8687) 2026-06-18 14:17:47 +01:00
renovate[bot]
b922346bb6
fix(deps): update dependency react-native-permissions to v5.5.3 (#8670)
Some checks are pending
Build Release and Upload to TestFlight (iOS) / build (push) Waiting to run
Build Release and Upload to TestFlight (iOS) / testflight-upload (push) Blocked by required conditions
BuildReleaseApk / buildReleaseApk (push) Waiting to run
BuildReleaseApk / browserstack (push) Blocked by required conditions
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Overtorment <overtorment@gmail.com>
2026-06-17 21:57:35 +01:00
Cursor Agent
64f1bd78db FIX: persist Arkade LNURL payment result
Co-authored-by: Overtorment <Overtorment@users.noreply.github.com>
2026-06-17 20:54:03 +01:00
Overtorment
1412a302a1 OPS: improve renovate bot 2026-06-17 17:57:39 +01:00
GLaDOS
6785427fe8
Merge pull request #8678 from BlueWallet/renovate/react-navigation-monorepo
fix(deps): update react-navigation monorepo
2026-06-17 17:47:50 +01:00
renovate[bot]
1f0ce7c813 chore(deps): update dependency @react-native/js-polyfills to ^0.86.0 2026-06-17 17:27:05 +01:00
Ramez Medhat
f5379795de ADD: support importing Unchained JSON as multisig cosigner
Some checks failed
Build Release and Upload to TestFlight (iOS) / build (push) Has been cancelled
BuildReleaseApk / buildReleaseApk (push) Has been cancelled
Build Release and Upload to TestFlight (iOS) / testflight-upload (push) Has been cancelled
BuildReleaseApk / browserstack (push) Has been cancelled
- Normalized `h` to `'` on all three derivation paths and accept either field name p2wsh_p2sh (Coldcard) or for p2sh_p2wsh (Unchained)
- Added a unit test for that.

Closes: https://github.com/BlueWallet/BlueWallet/issues/8251
2026-06-15 16:59:40 +01:00
renovate[bot]
7bc2c0e797
fix(deps): update react-navigation monorepo 2026-06-14 17:19:34 +00:00
Ojok Emmanuel Nsubuga
81cf0011b3
Merge branch 'master' into fix-custom-input-lag 2026-06-14 14:27:05 +03:00
Ojok Emmanuel Nsubuga
d259e68a85 FIX: Amount input lag in rbf custom fee input 2026-06-08 08:44:57 +03:00
297 changed files with 10642 additions and 5819 deletions

View File

@ -182,6 +182,7 @@ jobs:
- name: Install applesimutils
run: |
brew tap wix/brew
brew trust wix/brew
brew install applesimutils
- name: Download simulator app

View File

@ -1,37 +1,44 @@
[main]
host = https://www.transifex.com
# fastlane App Store metadata. lang_map remaps Transifex language codes to the
# App Store locale folder names fastlane deliver expects under fastlane/metadata/ios/<lang>/.
# All 4 resources share the same map.
[o:bluewallet:p:bluewallet-fastlane:r:ios-fastlane-metadata-en-us-description-txt--master]
file_filter = ios/fastlane/metadata/<lang>/description.txt
source_file = ios/fastlane/metadata/en-US/description.txt
file_filter = fastlane/metadata/ios/<lang>/description.txt
source_file = fastlane/metadata/ios/en-US/description.txt
source_lang = en_US
type = TXT
minimum_perc = 1
lang_map = fr_FR: fr-FR, nl_NL: nl-NL, pt_BR: pt-BR, zh_CN: zh-Hans, zh_HK: zh-Hant, ar_SA: ar-SA, es_MX: es-MX, fr_CA: fr-CA, pt_PT: pt-PT, de_DE: de-DE, es_ES: es-ES
lang_map = ar_SA: ar-SA, de_DE: de-DE, es_ES: es-ES, es_MX: es-MX, fr_CA: fr-CA, fr_FR: fr-FR, nl_NL: nl-NL, pt_BR: pt-BR, pt_PT: pt-PT, zh_CN: zh-Hans, zh_HK: zh-Hant
[o:bluewallet:p:bluewallet-fastlane:r:ios-fastlane-metadata-en-us-keywords-txt--master]
file_filter = ios/fastlane/metadata/<lang>/keywords.txt
source_file = ios/fastlane/metadata/en-US/keywords.txt
file_filter = fastlane/metadata/ios/<lang>/keywords.txt
source_file = fastlane/metadata/ios/en-US/keywords.txt
source_lang = en_US
type = TXT
minimum_perc = 1
lang_map = es_ES: es-ES, es_MX: es-MX, fr_CA: fr-CA, fr_FR: fr-FR, nl_NL: nl-NL, pt_BR: pt-BR, zh_CN: zh-Hans, ar_SA: ar-SA, de_DE: de-DE, pt_PT: pt-PT, zh_HK: zh-Hant
lang_map = ar_SA: ar-SA, de_DE: de-DE, es_ES: es-ES, es_MX: es-MX, fr_CA: fr-CA, fr_FR: fr-FR, nl_NL: nl-NL, pt_BR: pt-BR, pt_PT: pt-PT, zh_CN: zh-Hans, zh_HK: zh-Hant
[o:bluewallet:p:bluewallet-fastlane:r:ios-fastlane-metadata-en-us-name-txt--master]
file_filter = ios/fastlane/metadata/<lang>/name.txt
source_file = ios/fastlane/metadata/en-US/name.txt
file_filter = fastlane/metadata/ios/<lang>/name.txt
source_file = fastlane/metadata/ios/en-US/name.txt
source_lang = en_US
type = TXT
minimum_perc = 1
lang_map = zh_HK: zh-Hant, ar_SA: ar-SA, es_MX: es-MX, fr_FR: fr-FR, nl_NL: nl-NL, pt_BR: pt-BR, pt_PT: pt-PT, zh_CN: zh-Hans, de_DE: de-DE, es_ES: es-ES, fr_CA: fr-CA
lang_map = ar_SA: ar-SA, de_DE: de-DE, es_ES: es-ES, es_MX: es-MX, fr_CA: fr-CA, fr_FR: fr-FR, nl_NL: nl-NL, pt_BR: pt-BR, pt_PT: pt-PT, zh_CN: zh-Hans, zh_HK: zh-Hant
[o:bluewallet:p:bluewallet-fastlane:r:ios-fastlane-metadata-en-us-promotional-text-txt--master]
file_filter = ios/fastlane/metadata/<lang>/promotional_text.txt
source_file = ios/fastlane/metadata/en-US/promotional_text.txt
file_filter = fastlane/metadata/ios/<lang>/promotional_text.txt
source_file = fastlane/metadata/ios/en-US/promotional_text.txt
source_lang = en_US
type = TXT
minimum_perc = 1
lang_map = zh_CN: zh-Hans, zh_HK: zh-Hant, ar_SA: ar-SA, es_MX: es-MX, fr_CA: fr-CA, fr_FR: fr-FR, pt_PT: pt-PT, de_DE: de-DE, es_ES: es-ES, nl_NL: nl-NL, pt_BR: pt-BR
lang_map = ar_SA: ar-SA, de_DE: de-DE, es_ES: es-ES, es_MX: es-MX, fr_CA: fr-CA, fr_FR: fr-FR, nl_NL: nl-NL, pt_BR: pt-BR, pt_PT: pt-PT, zh_CN: zh-Hans, zh_HK: zh-Hant
# App UI strings. lang_map remaps Transifex language codes to the loc/<lang>.json
# filenames the app loads (separate, larger map than the fastlane resources above).
[o:bluewallet:p:bluewallet:r:loc-en-json--master]
file_filter = loc/<lang>.json

View File

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

View File

@ -87,7 +87,7 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.3.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

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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);
}

View File

@ -658,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);

View File

@ -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 (

View File

@ -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>;
@ -55,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: {
@ -72,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,
},
@ -93,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}
@ -114,7 +125,14 @@ const ListItem: React.FC<ListItemProps> = React.memo(
{rightTitle || rightSubtitle ? (
<View style={styles.rightColumn}>
{rightTitle ? (
<Text style={rightTitleStyle} numberOfLines={1} accessibilityRole="text" selectable={rightTitleSelectable}>
<Text
style={rightTitleStyle}
numberOfLines={1}
adjustsFontSizeToFit
minimumFontScale={0.75}
accessibilityRole="text"
selectable={rightTitleSelectable}
>
{rightTitle}
</Text>
) : null}
@ -192,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,

View File

@ -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}

View File

@ -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

View File

@ -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,

View File

@ -1,7 +1,7 @@
import React, { memo, useCallback, useMemo, useRef } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import Clipboard from '@react-native-clipboard/clipboard';
import { Animated, Easing, Linking, Pressable, Text, TextStyle, ViewStyle, StyleSheet, View } 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}

View File

@ -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',

View File

@ -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>
@ -541,7 +586,7 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
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(() => {
@ -650,7 +695,7 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
console.warn('[WalletsCarousel] Error scrolling to wallet:', error);
// Fallback: try scrolling to offset
// Use different measurement based on orientation
const itemSize = horizontal ? itemWidth : 195; // 195 is the approximate height of wallet card
const itemSize = horizontal ? itemWidth : WALLET_CAROUSEL_HEIGHT;
flatListRef.current.scrollToOffset({
offset: itemSize * walletIndex,
animated,
@ -772,7 +817,7 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
const keyExtractor = useCallback((item: TWallet, index: number) => (item?.getID ? item.getID() : index.toString()), []);
const sliderHeight = 195;
const sliderHeight = getWalletCarouselHeight(fontScale);
useEffect(() => {
return () => {
@ -855,7 +900,8 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
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,
@ -886,7 +932,7 @@ const WalletsCarousel = forwardRef<CarouselListRefType, WalletsCarouselProps>((p
automaticallyAdjustContentInsets
automaticallyAdjustKeyboardInsets
automaticallyAdjustsScrollIndicatorInsets
style={{ minHeight: sliderHeight + 12 }}
style={{ minHeight: sliderHeight }}
onScrollToIndexFailed={onScrollToIndexFailed}
ListFooterComponent={onNewWalletPress ? <NewWalletPanel onPress={onNewWalletPress} /> : null}
{...props}

View File

@ -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}

View File

@ -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',

View File

@ -0,0 +1 @@
../../ios/ar-SA/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/ar-SA/name.txt

View File

@ -0,0 +1 @@
../../ios/da/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/da/name.txt

View File

@ -0,0 +1 @@
../../ios/de-DE/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/de-DE/name.txt

View File

@ -0,0 +1 @@
../../ios/el/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/el/name.txt

View File

@ -1,38 +0,0 @@
A Bitcoin wallet that allows you to store, send Bitcoin, receive Bitcoin with focus on security and simplicity.
On BlueWallet, a bitcoin wallet you own you private keys. A Bitcoin wallet made by Bitcoin users for the community.
You can instantly transact with anyone in the world and transform the financial system right from your pocket.
Create for free unlimited number of bitcoin wallets or import your existing one on your Android device. It's simple and fast.
_____
Here's what you get:
1 - Security by design
• Open Source
MIT licensed, you can build it and run it on your own! Made with ReactNative
• Plausible deniability
Password which decrypts fake bitcoin wallets if you are forced to disclose your access
• Full encryption
On top of the iOS multi-layer encryption, we encrypt everything with added passwords
• SegWit & HD wallets
SegWit supported and HD wallets enable
2 - Focused on your experience
• Be in control
Private keys never leave your device.You control your private keys
• Flexible fees
Starting from 1 Satoshi. Defined by you, the user
• Replace-By-Fee
(RBF) Speed-up your transactions by increasing the fee (BIP125)
• Watch-only wallets
Watch-only wallets allow you to keep an eye on your cold storage without touching the hardware.
• Lightning Network
Lightning wallet with zero-configuration. Unfairly cheap and fast transactions with the best Bitcoin user experience.

View File

@ -0,0 +1 @@
../../ios/en-US/description.txt

View File

@ -1 +1 @@
Thin Bitcoin Wallet Built with React Native and Electrum
Radically simple, powerful & secure Bitcoin wallet. Lightning & open source.

View File

@ -1 +0,0 @@
BlueWallet Bitcoin Wallet

View File

@ -0,0 +1 @@
../../ios/en-US/name.txt

View File

@ -0,0 +1 @@
../../ios/es-MX/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/es-MX/name.txt

View File

@ -0,0 +1 @@
../../ios/es-ES/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/es-ES/name.txt

View File

@ -0,0 +1 @@
../../ios/fi/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/fi/name.txt

View File

@ -0,0 +1 @@
../../ios/fr-CA/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/fr-CA/name.txt

View File

@ -0,0 +1 @@
../../ios/fr-FR/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/fr-FR/name.txt

View File

@ -0,0 +1 @@
../../ios/hu/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/hu/name.txt

View File

@ -0,0 +1 @@
../../ios/id/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/id/name.txt

View File

@ -0,0 +1 @@
../../ios/it/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/it/name.txt

View File

@ -0,0 +1 @@
../../ios/he/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/he/name.txt

View File

@ -0,0 +1 @@
../../ios/ja/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/ja/name.txt

View File

@ -0,0 +1 @@
../../ios/ko/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/ko/name.txt

View File

@ -0,0 +1 @@
../../ios/ms/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/ms/name.txt

View File

@ -0,0 +1 @@
../../ios/nl-NL/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/nl-NL/name.txt

View File

@ -0,0 +1 @@
../../ios/no/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/no/name.txt

View File

@ -0,0 +1 @@
../../ios/pl/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/pl/name.txt

View File

@ -0,0 +1 @@
../../ios/pt-BR/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/pt-BR/name.txt

View File

@ -0,0 +1 @@
../../ios/pt-PT/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/pt-PT/name.txt

View File

@ -0,0 +1 @@
../../ios/ro/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/ro/name.txt

View File

@ -0,0 +1 @@
../../ios/ru/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/ru/name.txt

View File

@ -0,0 +1 @@
../../ios/sv/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/sv/name.txt

View File

@ -0,0 +1 @@
../../ios/th/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

View File

@ -0,0 +1 @@
../../ios/th/name.txt

View File

@ -0,0 +1 @@
../../ios/tr/description.txt

View File

@ -0,0 +1 @@
../en-US/short_description.txt

Some files were not shown because too many files have changed in this diff Show More