Compare commits
35 Commits
renovate/r
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5007f88085 | ||
|
|
6e783a1444 | ||
|
|
f86e560de4 | ||
|
|
7ea7fa7949 | ||
|
|
d2dfb1465b | ||
|
|
ce8c298d64 | ||
|
|
9d84def082 | ||
|
|
f00eabe2b3 | ||
|
|
193d9cfe94 | ||
|
|
1cf6289f76 | ||
|
|
cd4423c936 | ||
|
|
57f5ec5efd | ||
|
|
0cec7f6909 | ||
|
|
d437726e6a | ||
|
|
0549a2e6e2 | ||
|
|
c12b758c42 | ||
|
|
32d3f77f4f | ||
|
|
f26ff9189c | ||
|
|
1fa290652c | ||
|
|
099f6f46a6 | ||
|
|
01a11bc8dd | ||
|
|
6639891c24 | ||
|
|
4029d294f8 | ||
|
|
276a9ea8f8 | ||
|
|
d415f1a0b8 | ||
|
|
6124cf1c04 | ||
|
|
b922346bb6 | ||
|
|
64f1bd78db | ||
|
|
1412a302a1 | ||
|
|
6785427fe8 | ||
|
|
1f0ce7c813 | ||
|
|
f5379795de | ||
|
|
7bc2c0e797 | ||
|
|
81cf0011b3 | ||
|
|
d259e68a85 |
1
.github/workflows/e2e-ios.yml
vendored
1
.github/workflows/e2e-ios.yml
vendored
@ -182,6 +182,7 @@ jobs:
|
||||
- name: Install applesimutils
|
||||
run: |
|
||||
brew tap wix/brew
|
||||
brew trust wix/brew
|
||||
brew install applesimutils
|
||||
|
||||
- name: Download simulator app
|
||||
|
||||
31
.tx/config
31
.tx/config
@ -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
|
||||
|
||||
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,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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>;
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
@ -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}
|
||||
|
||||
@ -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',
|
||||
|
||||
1
fastlane/metadata/android/ar/full_description.txt
Symbolic link
1
fastlane/metadata/android/ar/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/ar-SA/description.txt
|
||||
1
fastlane/metadata/android/ar/short_description.txt
Symbolic link
1
fastlane/metadata/android/ar/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/ar/title.txt
Symbolic link
1
fastlane/metadata/android/ar/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/ar-SA/name.txt
|
||||
1
fastlane/metadata/android/da-DK/full_description.txt
Symbolic link
1
fastlane/metadata/android/da-DK/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/da/description.txt
|
||||
1
fastlane/metadata/android/da-DK/short_description.txt
Symbolic link
1
fastlane/metadata/android/da-DK/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/da-DK/title.txt
Symbolic link
1
fastlane/metadata/android/da-DK/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/da/name.txt
|
||||
1
fastlane/metadata/android/de-DE/full_description.txt
Symbolic link
1
fastlane/metadata/android/de-DE/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/de-DE/description.txt
|
||||
1
fastlane/metadata/android/de-DE/short_description.txt
Symbolic link
1
fastlane/metadata/android/de-DE/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/de-DE/title.txt
Symbolic link
1
fastlane/metadata/android/de-DE/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/de-DE/name.txt
|
||||
1
fastlane/metadata/android/el-GR/full_description.txt
Symbolic link
1
fastlane/metadata/android/el-GR/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/el/description.txt
|
||||
1
fastlane/metadata/android/el-GR/short_description.txt
Symbolic link
1
fastlane/metadata/android/el-GR/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/el-GR/title.txt
Symbolic link
1
fastlane/metadata/android/el-GR/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/el/name.txt
|
||||
@ -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.
|
||||
1
fastlane/metadata/android/en-US/full_description.txt
Symbolic link
1
fastlane/metadata/android/en-US/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/en-US/description.txt
|
||||
@ -1 +1 @@
|
||||
Thin Bitcoin Wallet Built with React Native and Electrum
|
||||
Radically simple, powerful & secure Bitcoin wallet. Lightning & open source.
|
||||
@ -1 +0,0 @@
|
||||
BlueWallet Bitcoin Wallet
|
||||
1
fastlane/metadata/android/en-US/title.txt
Symbolic link
1
fastlane/metadata/android/en-US/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/en-US/name.txt
|
||||
1
fastlane/metadata/android/es-419/full_description.txt
Symbolic link
1
fastlane/metadata/android/es-419/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/es-MX/description.txt
|
||||
1
fastlane/metadata/android/es-419/short_description.txt
Symbolic link
1
fastlane/metadata/android/es-419/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/es-419/title.txt
Symbolic link
1
fastlane/metadata/android/es-419/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/es-MX/name.txt
|
||||
1
fastlane/metadata/android/es-ES/full_description.txt
Symbolic link
1
fastlane/metadata/android/es-ES/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/es-ES/description.txt
|
||||
1
fastlane/metadata/android/es-ES/short_description.txt
Symbolic link
1
fastlane/metadata/android/es-ES/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/es-ES/title.txt
Symbolic link
1
fastlane/metadata/android/es-ES/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/es-ES/name.txt
|
||||
1
fastlane/metadata/android/fi-FI/full_description.txt
Symbolic link
1
fastlane/metadata/android/fi-FI/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/fi/description.txt
|
||||
1
fastlane/metadata/android/fi-FI/short_description.txt
Symbolic link
1
fastlane/metadata/android/fi-FI/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/fi-FI/title.txt
Symbolic link
1
fastlane/metadata/android/fi-FI/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/fi/name.txt
|
||||
1
fastlane/metadata/android/fr-CA/full_description.txt
Symbolic link
1
fastlane/metadata/android/fr-CA/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/fr-CA/description.txt
|
||||
1
fastlane/metadata/android/fr-CA/short_description.txt
Symbolic link
1
fastlane/metadata/android/fr-CA/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/fr-CA/title.txt
Symbolic link
1
fastlane/metadata/android/fr-CA/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/fr-CA/name.txt
|
||||
1
fastlane/metadata/android/fr-FR/full_description.txt
Symbolic link
1
fastlane/metadata/android/fr-FR/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/fr-FR/description.txt
|
||||
1
fastlane/metadata/android/fr-FR/short_description.txt
Symbolic link
1
fastlane/metadata/android/fr-FR/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/fr-FR/title.txt
Symbolic link
1
fastlane/metadata/android/fr-FR/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/fr-FR/name.txt
|
||||
1
fastlane/metadata/android/hu-HU/full_description.txt
Symbolic link
1
fastlane/metadata/android/hu-HU/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/hu/description.txt
|
||||
1
fastlane/metadata/android/hu-HU/short_description.txt
Symbolic link
1
fastlane/metadata/android/hu-HU/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/hu-HU/title.txt
Symbolic link
1
fastlane/metadata/android/hu-HU/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/hu/name.txt
|
||||
1
fastlane/metadata/android/id/full_description.txt
Symbolic link
1
fastlane/metadata/android/id/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/id/description.txt
|
||||
1
fastlane/metadata/android/id/short_description.txt
Symbolic link
1
fastlane/metadata/android/id/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/id/title.txt
Symbolic link
1
fastlane/metadata/android/id/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/id/name.txt
|
||||
1
fastlane/metadata/android/it-IT/full_description.txt
Symbolic link
1
fastlane/metadata/android/it-IT/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/it/description.txt
|
||||
1
fastlane/metadata/android/it-IT/short_description.txt
Symbolic link
1
fastlane/metadata/android/it-IT/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/it-IT/title.txt
Symbolic link
1
fastlane/metadata/android/it-IT/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/it/name.txt
|
||||
1
fastlane/metadata/android/iw-IL/full_description.txt
Symbolic link
1
fastlane/metadata/android/iw-IL/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/he/description.txt
|
||||
1
fastlane/metadata/android/iw-IL/short_description.txt
Symbolic link
1
fastlane/metadata/android/iw-IL/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/iw-IL/title.txt
Symbolic link
1
fastlane/metadata/android/iw-IL/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/he/name.txt
|
||||
1
fastlane/metadata/android/ja-JP/full_description.txt
Symbolic link
1
fastlane/metadata/android/ja-JP/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/ja/description.txt
|
||||
1
fastlane/metadata/android/ja-JP/short_description.txt
Symbolic link
1
fastlane/metadata/android/ja-JP/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/ja-JP/title.txt
Symbolic link
1
fastlane/metadata/android/ja-JP/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/ja/name.txt
|
||||
1
fastlane/metadata/android/ko-KR/full_description.txt
Symbolic link
1
fastlane/metadata/android/ko-KR/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/ko/description.txt
|
||||
1
fastlane/metadata/android/ko-KR/short_description.txt
Symbolic link
1
fastlane/metadata/android/ko-KR/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/ko-KR/title.txt
Symbolic link
1
fastlane/metadata/android/ko-KR/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/ko/name.txt
|
||||
1
fastlane/metadata/android/ms/full_description.txt
Symbolic link
1
fastlane/metadata/android/ms/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/ms/description.txt
|
||||
1
fastlane/metadata/android/ms/short_description.txt
Symbolic link
1
fastlane/metadata/android/ms/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/ms/title.txt
Symbolic link
1
fastlane/metadata/android/ms/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/ms/name.txt
|
||||
1
fastlane/metadata/android/nl-NL/full_description.txt
Symbolic link
1
fastlane/metadata/android/nl-NL/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/nl-NL/description.txt
|
||||
1
fastlane/metadata/android/nl-NL/short_description.txt
Symbolic link
1
fastlane/metadata/android/nl-NL/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/nl-NL/title.txt
Symbolic link
1
fastlane/metadata/android/nl-NL/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/nl-NL/name.txt
|
||||
1
fastlane/metadata/android/no-NO/full_description.txt
Symbolic link
1
fastlane/metadata/android/no-NO/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/no/description.txt
|
||||
1
fastlane/metadata/android/no-NO/short_description.txt
Symbolic link
1
fastlane/metadata/android/no-NO/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/no-NO/title.txt
Symbolic link
1
fastlane/metadata/android/no-NO/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/no/name.txt
|
||||
1
fastlane/metadata/android/pl-PL/full_description.txt
Symbolic link
1
fastlane/metadata/android/pl-PL/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/pl/description.txt
|
||||
1
fastlane/metadata/android/pl-PL/short_description.txt
Symbolic link
1
fastlane/metadata/android/pl-PL/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/pl-PL/title.txt
Symbolic link
1
fastlane/metadata/android/pl-PL/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/pl/name.txt
|
||||
1
fastlane/metadata/android/pt-BR/full_description.txt
Symbolic link
1
fastlane/metadata/android/pt-BR/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/pt-BR/description.txt
|
||||
1
fastlane/metadata/android/pt-BR/short_description.txt
Symbolic link
1
fastlane/metadata/android/pt-BR/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/pt-BR/title.txt
Symbolic link
1
fastlane/metadata/android/pt-BR/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/pt-BR/name.txt
|
||||
1
fastlane/metadata/android/pt-PT/full_description.txt
Symbolic link
1
fastlane/metadata/android/pt-PT/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/pt-PT/description.txt
|
||||
1
fastlane/metadata/android/pt-PT/short_description.txt
Symbolic link
1
fastlane/metadata/android/pt-PT/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/pt-PT/title.txt
Symbolic link
1
fastlane/metadata/android/pt-PT/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/pt-PT/name.txt
|
||||
1
fastlane/metadata/android/ro/full_description.txt
Symbolic link
1
fastlane/metadata/android/ro/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/ro/description.txt
|
||||
1
fastlane/metadata/android/ro/short_description.txt
Symbolic link
1
fastlane/metadata/android/ro/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/ro/title.txt
Symbolic link
1
fastlane/metadata/android/ro/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/ro/name.txt
|
||||
1
fastlane/metadata/android/ru-RU/full_description.txt
Symbolic link
1
fastlane/metadata/android/ru-RU/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/ru/description.txt
|
||||
1
fastlane/metadata/android/ru-RU/short_description.txt
Symbolic link
1
fastlane/metadata/android/ru-RU/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/ru-RU/title.txt
Symbolic link
1
fastlane/metadata/android/ru-RU/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/ru/name.txt
|
||||
1
fastlane/metadata/android/sv-SE/full_description.txt
Symbolic link
1
fastlane/metadata/android/sv-SE/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/sv/description.txt
|
||||
1
fastlane/metadata/android/sv-SE/short_description.txt
Symbolic link
1
fastlane/metadata/android/sv-SE/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/sv-SE/title.txt
Symbolic link
1
fastlane/metadata/android/sv-SE/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/sv/name.txt
|
||||
1
fastlane/metadata/android/th/full_description.txt
Symbolic link
1
fastlane/metadata/android/th/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/th/description.txt
|
||||
1
fastlane/metadata/android/th/short_description.txt
Symbolic link
1
fastlane/metadata/android/th/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
1
fastlane/metadata/android/th/title.txt
Symbolic link
1
fastlane/metadata/android/th/title.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/th/name.txt
|
||||
1
fastlane/metadata/android/tr-TR/full_description.txt
Symbolic link
1
fastlane/metadata/android/tr-TR/full_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../../ios/tr/description.txt
|
||||
1
fastlane/metadata/android/tr-TR/short_description.txt
Symbolic link
1
fastlane/metadata/android/tr-TR/short_description.txt
Symbolic link
@ -0,0 +1 @@
|
||||
../en-US/short_description.txt
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user