Compare commits

...

575 Commits

Author SHA1 Message Date
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
Ivan Vershigora
c76db2f84a tst: fix flaky goBack in iOS e2e tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 16:20:46 +01:00
Andrew Camilleri (Kukks)
cb6e3ae69b OPS: bump Arkade SDKs (@arkade-os/sdk 0.4.35, @arkade-os/boltz-swap 0.3.40) 2026-06-15 16:12:52 +01:00
renovate[bot]
7bc2c0e797
fix(deps): update react-navigation monorepo 2026-06-14 17:19:34 +00:00
GLaDOS
5a7c514548
Merge pull request #8617 from BlueWallet/renovate/react-native-vector-icons-entypo-13.x
Some checks are pending
BuildReleaseApk / browserstack (push) Blocked by required conditions
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
fix(deps): update dependency @react-native-vector-icons/entypo to v13.1.2
2026-06-14 18:14:01 +01:00
Ojok Emmanuel Nsubuga
81cf0011b3
Merge branch 'master' into fix-custom-input-lag 2026-06-14 14:27:05 +03:00
Ivan Vershigora
94062ffc9f fix: improve typescript coverage
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
2026-06-14 11:28:25 +01:00
GLaDOS
0449a25c6a
Merge pull request #8669 from BlueWallet/renovate/dayjs-1.x
Some checks are pending
BuildReleaseApk / browserstack (push) Blocked by required conditions
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
fix(deps): update dependency dayjs to v1.11.21
2026-06-13 19:06:04 +01:00
renovate[bot]
ff98ca0c1c
fix(deps): update dependency dayjs to v1.11.21 2026-06-13 16:00:26 +00:00
GLaDOS
c9dcbf40e7
Merge pull request #8653 from BlueWallet/renovate/react-native-vector-icons-material-icons-13.x
fix(deps): update dependency @react-native-vector-icons/material-icons to v13.1.2
2026-06-13 16:51:33 +01:00
GLaDOS
c8a7887808
Merge pull request #8649 from BlueWallet/renovate/react-native-vector-icons-fontawesome6-13.x
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
fix(deps): update dependency @react-native-vector-icons/fontawesome6 to v13.1.2
2026-06-13 03:21:50 +01:00
Marcos Rodriguez Vélez
e4504b2355
FIX: Remove chevron on modal open ux (#8651)
* FIX: Remove chevron on modal open ux

* Update WalletDetails.tsx
2026-06-13 03:21:46 +01:00
renovate[bot]
13cedbe49e
fix(deps): update dependency @react-native-vector-icons/material-icons to v13.1.2 2026-06-12 17:26:13 +00:00
GLaDOS
abb80665af
Merge pull request #8652 from BlueWallet/renovate/react-native-vector-icons-material-design-icons-13.x
fix(deps): update dependency @react-native-vector-icons/material-design-icons to v13.1.2
2026-06-12 18:12:58 +01:00
renovate[bot]
681cbcf2dc
fix(deps): update dependency @react-native-vector-icons/material-design-icons to v13.1.2 2026-06-12 05:30:13 +00:00
renovate[bot]
78c9d49359
fix(deps): update dependency @react-native-vector-icons/fontawesome6 to v13.1.2 2026-06-12 05:30:02 +00:00
renovate[bot]
da606dbff0
fix(deps): update dependency @react-native-vector-icons/entypo to v13.1.2 2026-06-12 05:29:49 +00:00
GLaDOS
8fda883933
Merge pull request #8648 from BlueWallet/renovate/react-native-vector-icons-fontawesome-13.x
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
fix(deps): update dependency @react-native-vector-icons/fontawesome to v13.1.2
2026-06-12 06:24:27 +01:00
GLaDOS
c4abe9562e
Merge pull request #8650 from BlueWallet/renovate/react-native-vector-icons-ionicons-13.x
fix(deps): update dependency @react-native-vector-icons/ionicons to v13.1.2
2026-06-12 06:24:22 +01:00
renovate[bot]
5d8e605fe7
fix(deps): update dependency @react-native-vector-icons/ionicons to v13.1.2 2026-06-12 00:17:10 +00:00
GLaDOS
387f8dffd6
Merge pull request #8646 from BlueWallet/renovate/arkade-os-boltz-swap-0.x
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
fix(deps): update dependency @arkade-os/boltz-swap to v0.3.38
2026-06-12 01:10:04 +01:00
renovate[bot]
b34f52fdce
fix(deps): update dependency @arkade-os/boltz-swap to v0.3.38 2026-06-11 21:40:26 +00:00
GLaDOS
a3c12a27eb
Merge pull request #8647 from BlueWallet/renovate/arkade-os-sdk-0.x
fix(deps): update dependency @arkade-os/sdk to v0.4.33
2026-06-11 22:28:44 +01:00
Ivan Vershigora
a04cad686f fix: comment with the reference 2026-06-11 19:52:13 +01:00
D N
61877ed0db No babel duplicates 2026-06-11 19:52:13 +01:00
renovate[bot]
859877979e
fix(deps): update dependency @react-native-vector-icons/fontawesome to v13.1.2 2026-06-11 17:58:31 +00:00
GLaDOS
71c76bd8c8
Merge pull request #8643 from BlueWallet/renovate/actions-checkout-6.x
chore(deps): update actions/checkout action to v6.0.3
2026-06-11 18:44:41 +01:00
renovate[bot]
dd52747191
fix(deps): update dependency @arkade-os/sdk to v0.4.33 2026-06-11 16:45:01 +00:00
renovate[bot]
fa9434b0de
fix(deps): update dependency @noble/secp256k1 to v1.7.2 (#8590)
* fix(deps): update dependency @noble/secp256k1 to v1.7.2

* fix(deps): secp

* fix(deps): secp

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Overtorment <overtorment@gmail.com>
2026-06-11 17:27:52 +01:00
GLaDOS
770a7a4075
Merge pull request #8639 from BlueWallet/perf-hd-fetch-transactions
REF: single-pass tx-to-address mapping in HD fetchTransactions
2026-06-11 14:37:51 +01:00
renovate[bot]
f6f0238e0a
chore(deps): update actions/checkout action to v6.0.3 2026-06-11 11:47:08 +00:00
Ivan Vershigora
236791f32e
REF: single-pass tx-to-address mapping in HD fetchTransactions
Mapping fetched transactions into per-address cells re-scanned every
transaction for every address index, and the upsert-by-txid step
re-scanned the target cell for every insert — both quadratic. Build
address -> index lookup maps once, do a single pass over fetched
transactions, and keep a per-cell txid -> position map for
constant-time upserts.

Measured on a wallet emulating one address with 63,017 real
transactions (genesis address), median of 3 runs on real chain data:
mapping went from 32.1s to 0.47s.

Also remove a duplicate @babel/preset-env entry from devDependencies:
it is pinned in dependencies (needed by the --omit=dev iOS release
build), so the second entry was ignored by npm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 10:54:15 +01:00
Overtorment
0181f0a849
ADD: arkade ln pushes (#8634)
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
2026-06-10 17:35:17 +01:00
Cursor Agent
f334b985e8 REF: remove GroundControl server URI saving from notifications module
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: Overtorment <Overtorment@users.noreply.github.com>
2026-06-10 12:44:18 +01:00
Cursor Agent
bfeda40284 REF: remove GroundControl server URL option from notification settings
Co-authored-by: Overtorment <Overtorment@users.noreply.github.com>
2026-06-10 12:44:18 +01:00
Ivan Vershigora
a4e224ec96 fix: no round corners in qrcode
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-08 22:25:34 +01:00
Ojok Emmanuel Nsubuga
d259e68a85 FIX: Amount input lag in rbf custom fee input 2026-06-08 08:44:57 +03:00
Marcos Rodriguez
926a863d27 OPS: Bump version
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
2026-06-04 22:03:02 +01:00
GLaDOS
f0336e1789
Merge pull request #8618 from BlueWallet/fix-startup-crash
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: startup crash
2026-06-02 14:01:21 +01:00
Overtorment
2c8cfd3690 FIX: startup crash 2026-06-01 18:16:27 +01:00
pietro909
e37c4a693c
OPS: upgrade Arkade SDKs and harden Ark wallet integration (#8585)
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
2026-06-01 15:22:17 +01:00
GladosBlueWallet
2a3de6f473 OPS: swap GroundControl production URL to groundcontrol.bluewallet.io 2026-06-01 11:08:57 +01:00
GLaDOS
bbd6101ddb
OPS: bump detox to 20.51.3 (#8613)
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
* OPS: bump detox to 20.51.3

* OPS: pin detox to exact version 20.51.3
2026-05-31 13:51:45 +01:00
Marcos Rodriguez Vélez
687e007c56
OPS: Bump Nav packages (#8573)
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
* OPS: Bump Nav packages

* OPS: Package update

---------

Co-authored-by: Overtorment <overtorment@gmail.com>
2026-05-28 23:03:43 +01:00
GLaDOS
0641bf70cd
Merge pull request #8588 from BlueWallet/renovate/bugsnag-js-monorepo
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
fix(deps): update dependency @bugsnag/react-native to v8.9.0
2026-05-28 20:35:00 +01:00
renovate[bot]
cbe6ebb423
fix(deps): update dependency @bugsnag/react-native to v8.9.0 2026-05-28 14:09:13 +00:00
Overtorment
e19ce1136f REF: electrum 2026-05-28 13:04:49 +01:00
dependabot[bot]
1a88c085ec build(deps): bump tmp from 0.2.5 to 0.2.7
Bumps [tmp](https://github.com/raszi/node-tmp) from 0.2.5 to 0.2.7.
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.2.5...v0.2.7)

---
updated-dependencies:
- dependency-name: tmp
  dependency-version: 0.2.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-28 10:40:55 +01:00
PeterXMR
d7261d4d2a FIX: ElectrumTransaction confirmation fields are optional until mined (#8093)
Electrum's `blockchain.transaction.get` verbose response does not include
`blockhash`, `confirmations`, `time`, or `blocktime` when the transaction
is still in the mempool. Both `ElectrumTransaction` in
`blue_modules/BlueElectrum.ts` and the sibling `Transaction` type in
`class/wallets/types.ts` declared all four as required, which silently
let unguarded access compile and crash at runtime on real mempool data.

- Mark the four confirmation-only fields optional on both types. They
  describe the same shape and have the same bug.
- Export `ElectrumTransaction` so a regression test can reference it.
- Collapse the two-line `tx.timestamp = tx.blocktime; if (!tx.blocktime)
  tx.timestamp = ...` pattern in `abstract-hd-electrum-wallet.ts` into a
  single `||` fallback — type-safe and runtime-equivalent.
- Add nullish-coalesce guards at the two call sites that compared
  `confirmations` directly to a number. In `useWidgetCommunication.ios.ts`,
  `t.confirmations ?? 0` keeps the filter semantically unchanged. In
  `PaymentCodesList.tsx`, normalize once via
  `notificationTx?.confirmations ?? 0` and use the local in both the
  `> 0` (already confirmed) and `=== 0` (mempool / unconfirmed alert)
  branches — otherwise a mempool notification tx would skip both branches
  and the code would create a duplicate notification transaction.
- Add `tests/unit/electrum-transaction-types.test.ts` documenting that a
  mempool-shaped object satisfies the type.
2026-05-28 09:43:46 +01:00
GLaDOS
4aa07ed904
Merge pull request #8591 from BlueWallet/ref-managewallets-2
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
tst: manage wallets
2026-05-27 22:22:18 +01:00
Marcos Rodriguez
b315d587ff FIX: Manage funds layout 2026-05-27 20:43:00 +01:00
Ivan Vershigora
d367a2f383 FIX: setDisabled aborts in-flight ensureConnected; keep disabled state through abort; add tests 2026-05-27 20:40:07 +01:00
Overtorment
e4c8a3057d REF: blue electrum 2026-05-27 20:40:07 +01:00
Overtorment
1109a836e9 REF: blue electrum 2026-05-27 20:40:07 +01:00
Overtorment
8e62aee2fc REF: blue electrum 2026-05-27 20:40:07 +01:00
Overtorment
220cd7e61d REF: blue electrum 2026-05-27 20:40:07 +01:00
Ivan Vershigora
d043b86310
tst: manage wallets 2026-05-27 19:23:55 +01:00
GLaDOS
8310c5f9fa
Merge pull request #8601 from BlueWallet/fix-amount-input
FIX: swap AmountInput zIndex so TextInput sits on top
2026-05-27 19:14:06 +01:00
Ivan Vershigora
8599607574
FIX: swap AmountInput zIndex so TextInput sits on top 2026-05-27 12:31:48 +01:00
dependabot[bot]
1a0ac1188e build(deps): bump qs from 6.15.0 to 6.15.2
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
Bumps [qs](https://github.com/ljharb/qs) from 6.15.0 to 6.15.2.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.15.0...v6.15.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.15.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-23 15:59:27 +01:00
GLaDOS
66ca6a9fca
Merge pull request #8583 from BlueWallet/renovate/fastlane-2.x
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
chore(deps): update dependency fastlane to "~> 2.234.0"
2026-05-22 13:05:33 +01:00
GLaDOS
90fdee2a8f
Merge pull request #8581 from BlueWallet/renovate/lottie-react-native-7.x
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
fix(deps): update dependency lottie-react-native to v7.3.8
2026-05-22 00:58:37 +01:00
renovate[bot]
d14556ff80
fix(deps): update react-navigation monorepo (#8582)
* fix(deps): update react-navigation monorepo

* OPS: Ruby

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Marcos Rodriguez <marcospr@pm.me>
2026-05-22 00:32:17 +01:00
GLaDOS
047b675faf
Merge pull request #8543 from BlueWallet/renovate/react-native-reanimated-4.x
fix(deps): update dependency react-native-reanimated to v4.3.1
2026-05-22 00:04:06 +01:00
GLaDOS
7085dd4a81
Merge pull request #8565 from BlueWallet/deps
REF: prune unused deps + stale tsconfig entries
2026-05-21 22:56:49 +01:00
renovate[bot]
8c58df270a
chore(deps): update dependency fastlane to "~> 2.234.0" 2026-05-21 21:23:19 +00:00
Marcos Rodriguez Vélez
3952ec7e12
ADD: If lndhub server is set then button should be visible (#8572)
Co-authored-by: Overtorment <overtorment@gmail.com>
2026-05-21 22:21:41 +01:00
GLaDOS
26b6419507
Merge pull request #8578 from BlueWallet/prompt
ref: prompt
2026-05-21 21:46:52 +01:00
renovate[bot]
964ceecd6a
fix(deps): update dependency lottie-react-native to v7.3.8 2026-05-21 20:02:57 +00:00
renovate[bot]
df782c5f7a chore(deps): update dependency ruby to v3.4.9 2026-05-21 21:01:23 +01:00
Overtorment
f7c72e13d4
Merge branch 'master' into renovate/react-native-reanimated-4.x 2026-05-21 21:00:59 +01:00
renovate[bot]
9e66ed003a fix(deps): update electrum-client digest to 83420b8 2026-05-21 21:00:16 +01:00
renovate[bot]
08c4c02491 fix(deps): update dependency @babel/preset-env to v7.29.5 2026-05-21 20:59:49 +01:00
renovate[bot]
3c199ab9f2 fix(deps): update react-native-capture-protection digest to b17b9ec 2026-05-21 20:57:37 +01:00
GLaDOS
0881efbd40
Merge pull request #8580 from BlueWallet/fix-inputamount-2
fix: styles in AmountInput
2026-05-21 17:02:06 +01:00
Ivan Vershigora
a5a7d34478 FIX: patch react-native-tcp-socket onConnect crash; stabilize iOS e2e 2026-05-21 16:33:43 +01:00
Ivan Vershigora
c8344e6037
ref: prompt 2026-05-21 13:29:42 +01:00
Ivan Vershigora
54c5f34e11
fix: styles in AmountInput 2026-05-21 12:53:41 +01:00
Nuno
431a8006ea
fix: center amount input (#8574) 2026-05-21 12:18:52 +02:00
Nuno
092c437557
fix: fee jump on mount (#8575) 2026-05-21 12:17:39 +02:00
dependabot[bot]
f65ccb5427 build(deps-dev): bump @babel/plugin-transform-modules-systemjs
Bumps [@babel/plugin-transform-modules-systemjs](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-modules-systemjs) from 7.29.0 to 7.29.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.29.4/packages/babel-plugin-transform-modules-systemjs)

---
updated-dependencies:
- dependency-name: "@babel/plugin-transform-modules-systemjs"
  dependency-version: 7.29.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 11:09:37 +01:00
Nuno
aa5a6ba7f0
fix: inputs alignments (#8570) 2026-05-21 09:14:38 +02:00
Nuno
9e907566f0
feat: redesigned wallet details (#8301)
* feat: redesign transaction detail screen with unified layout and Lottie pending animation

* ADD: decode OP_RETURN payload as UTF-8 text in transaction detail

Co-authored-by: Cursor <cursoragent@cursor.com>

* REF: transaction detail redesign (themes, pending icon, loc)

Co-authored-by: Cursor <cursoragent@cursor.com>

* REF: remove deprecated TransactionDetails, TransactionStatus and getTransactionStatusOptions

Co-authored-by: Cursor <cursoragent@cursor.com>

* FIX: resolve lint errors (unused vars, styles, loc keys, no-bitwise, inline styles)

Co-authored-by: Cursor <cursoragent@cursor.com>

* FIX: remove redundant !tx check in transaction detail guard

Co-authored-by: Cursor <cursoragent@cursor.com>

* FIX: show transaction not available when tx not found after load

Co-authored-by: Cursor <cursoragent@cursor.com>

* FIX: remove unused transaction prop type from TransactionDetail

Co-authored-by: Cursor <cursoragent@cursor.com>

* TST: update UTXO note E2E to use new transaction detail note prompt UI

Co-authored-by: Cursor <cursoragent@cursor.com>

* simplify changes on the PR for review

* remove unused loc

* remove unchanged colors

* better offline support for tx details

* remove unused key loc

* fix code review issues

* fix balance

* fix tests

* REF: address PR #8289 review feedback

* redesigned wallets details

* fix lint

* fix lint

* fix bip84 test

* fix test

* fix tests

* fix tests

* fix: truncation and sendTo logic display

* fix: new arch fixes

* fix: lint

* fix: crash on status update

* fix: lint and tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* Potential fix for pull request finding 'Identical operands'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix style

* fix merge master

* Merge branch 'wallet-details' of https://github.com/BlueWallet/BlueWallet into wallet-details

* fix loc

* fix loc

* fix style

* improve coin control from wallet details

* fix: e2e

* fix: WalletDetails

* fix: flat

* fix: e2e

* fix: e2e

* Potential fix for pull request finding 'Unused variable, import, function or class'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: remove notifications dialogs

* fix: second button title

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Ivan Vershigora <ivan.vershigora@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Overtorment <overtorment@gmail.com>
2026-05-21 09:12:49 +02:00
Ivan Vershigora
265cebef62 REF: prune unused deps + stale tsconfig entries 2026-05-20 18:40:25 +01:00
GLaDOS
c1c13e9e58
Merge pull request #8577 from BlueWallet/fix-wakeup-revert
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: revert wake up
2026-05-20 17:37:13 +01:00
Ivan Vershigora
87b2bb2156
fix: revert wake up 2026-05-20 14:54:11 +01:00
GLaDOS
7a5589eb00
Merge pull request #8564 from BlueWallet/components
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: split BlueComponents, prune dead/optional props
2026-05-19 22:16:24 +01:00
GLaDOS
87cf8a5600
Merge pull request #8559 from BlueWallet/i18-lazy
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: lazy-load translations
2026-05-19 16:53:23 +01:00
Ojok Emmanuel Nsubuga
1da0414474
ADD: Import private keys in hex or base64 formats 2026-05-19 13:18:31 +01:00
GLaDOS
151dbbbc67
Merge pull request #8561 from BlueWallet/e2e-simplify
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
tst: simplify wallet discovery and import test
2026-05-18 19:32:10 +01:00
renovate[bot]
bcc393dca8
fix(deps): update dependency react-native-reanimated to v4.3.1 2026-05-18 12:13:49 +00:00
Ivan Vershigora
1766cadcdf
REF: split BlueComponents, prune dead/optional props 2026-05-18 12:56:12 +01:00
Ivan Vershigora
4524882015
tst: simplify wallet discovery and import test 2026-05-18 10:29:13 +01:00
GLaDOS
4429f127c6
Merge pull request #8562 from BlueWallet/renovate/lottie-react-native-7.x
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
fix(deps): update dependency lottie-react-native to v7.3.7
2026-05-18 00:49:27 +01:00
Ivan Vershigora
44b5640ff3
ref: lazy-load translations 2026-05-17 22:02:20 +01:00
renovate[bot]
8d8b2fb9cf
fix(deps): update dependency lottie-react-native to v7.3.7 2026-05-17 20:13:51 +00:00
GLaDOS
714984cefe
Merge pull request #8558 from BlueWallet/renovate/react-native-svg-15.x
fix(deps): update dependency react-native-svg to v15.15.5
2026-05-17 21:08:54 +01:00
GLaDOS
42b1154543
Merge pull request #8541 from BlueWallet/amouunt
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: amountinput layout
2026-05-16 00:56:27 +01:00
Marcos Rodriguez
492a617d42 Merge branch 'amouunt' of https://github.com/BlueWallet/BlueWallet into amouunt 2026-05-15 16:02:30 -05:00
Marcos Rodriguez
ebf8e245ec w 2026-05-15 16:02:24 -05:00
Overtorment
14b3695a36
Merge branch 'master' into amouunt 2026-05-15 21:49:39 +01:00
renovate[bot]
f3294d1fc6
fix(deps): update dependency react-native-svg to v15.15.5 2026-05-15 15:17:52 +00:00
GLaDOS
ae9a5605f3
Merge pull request #8535 from BlueWallet/renovate/react-native-gesture-handler-2.x
fix(deps): update dependency react-native-gesture-handler to v2.31.2
2026-05-15 16:13:30 +01:00
GLaDOS
0749457c04
Merge pull request #8542 from BlueWallet/e2e-stability
tst: e2e stability
2026-05-15 16:13:15 +01:00
GLaDOS
4fe998aad8
Merge pull request #8545 from BlueWallet/locsync31
ref: localization sweep — translations, lint script, cleanups
2026-05-15 16:13:05 +01:00
GLaDOS
5f865777c9
Merge pull request #8546 from BlueWallet/cursor-fix-tx-details-fee-rate-decimal-585e
FIX: transaction details fee rate to one decimal place
2026-05-15 16:13:00 +01:00
GLaDOS
66736f9c91
Merge pull request #8532 from BlueWallet/dependabot/npm_and_yarn/fast-xml-builder-1.2.0
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
build(deps): bump fast-xml-builder from 1.1.4 to 1.2.0
2026-05-14 22:35:34 +01:00
Ivan Vershigora
fef40b4990
fix: cleanup 2026-05-14 16:37:30 +01:00
Ivan Vershigora
8b16c53e5c
ref: localization sweep — translations, lint script, cleanups 2026-05-14 16:27:30 +01:00
Marcos Rodriguez
32f810cb46 Update StorageProvider.tsx 2026-05-13 16:51:05 -05:00
Marcos Rodriguez
fb8fd51d86 Update StorageProvider.tsx 2026-05-13 16:49:22 -05:00
Marcos Rodriguez
71dd93ede2 FIX: amountinput layout 2026-05-13 14:49:39 -05:00
Nuno
1728f33f0a
ref: sign and verify screen (#8548)
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
2026-05-13 20:27:11 +02:00
Ivan Vershigora
295a32caef tst: e2e stability 2026-05-13 18:47:12 +01:00
Nuno
8195855f05
fix: wake up (#8537)
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
2026-05-13 16:34:14 +02:00
Nuno
d09bd69b96
fix: sats format (#8551)
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
2026-05-13 07:24:53 +02:00
renovate[bot]
ccb1dcaef7
fix(deps): update dependency react-native-gesture-handler to v2.31.2 2026-05-12 12:09:38 +00:00
Cursor Agent
3a48182105
FIX: show transaction fee rate with one decimal on details screen
Fee rate was computed with Math.round, collapsing values like 3.6 sats/vB
to 4. Round to one decimal place for display and copy text.

Co-authored-by: Overtorment <Overtorment@users.noreply.github.com>
2026-05-11 16:22:37 +00:00
Nuno
e2bcae3818
ADD: Redesign txdetail (#8289)
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-05-11 17:10:49 +01:00
Ivan Vershigora
f039c993cd OPS: pin action versions, prebuild RN core, warm iOS sim for e2e 2026-05-11 12:52:23 +01:00
renovate[bot]
11d367b975 fix(deps): update dependency bitcoinjs-lib to v7.0.1 2026-05-11 12:49:03 +01:00
renovate[bot]
31820f4cad fix(deps): update dependency ecpair to v3.0.1 2026-05-11 12:48:18 +01:00
Nuno
167dc05cdf
ref: manage wallets list and search (#8527)
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: manage wallets list and search

* fix review

* improvements

* fix: svg and localization

* fix: swipe

* fix: dark mode

* fix: props type

* fix: remove dead code

* fix: drop TouchableWithoutFeedback

* fix: usePreventRemove

* ref: address search

* fix header animation

* fix: code style

---------

Co-authored-by: Ivan Vershigora <ivan.vershigora@gmail.com>
2026-05-10 17:59:12 +02:00
Nuno
8276b8d22b
ref: floatbuttons borders and animation (#8529)
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
2026-05-09 18:57:08 +02:00
Overtorment
edff2468d4
Merge branch 'master' into dependabot/npm_and_yarn/fast-xml-builder-1.2.0 2026-05-09 13:07:41 +01:00
Overtorment
55f7004440
OPS: speed up CI, split e2e workflows per platform 2026-05-09 12:57:36 +01:00
Ivan Vershigora
5e7f61fba4
fix: Podfile.lock 2026-05-09 09:33:04 +01:00
Ivan Vershigora
6c23fdc0d4
fix: sync with master 2026-05-09 09:17:10 +01:00
GLaDOS
ae24973596
Merge pull request #8528 from BlueWallet/853
OPS: Update RN
2026-05-09 02:04:47 +01:00
dependabot[bot]
620553d0a2
build(deps): bump fast-xml-builder from 1.1.4 to 1.2.0
Bumps [fast-xml-builder](https://github.com/NaturalIntelligence/fast-xml-builder) from 1.1.4 to 1.2.0.
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-builder/blob/main/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-builder/compare/v1.1.4...v1.2.0)

---
updated-dependencies:
- dependency-name: fast-xml-builder
  dependency-version: 1.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 16:45:54 +00:00
Marcos Rodriguez
6256258fa0 OPS: detox 2026-05-06 23:21:00 -05:00
Ivan Vershigora
e9cd4262dd
OPS: split e2e workflows per platform, add build/test split, tune CI caches 2026-05-06 13:29:34 +01:00
Marcos Rodriguez
4181019ad3 OPS: Update RN 2026-05-05 15:42:04 -05:00
Nuno
7560f92c4b
feat: status pills on the header (#8418)
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
* feat: status pills on the header

* ref: review suggestions

* ref: speed

* optimize

* comments

* conflicts

* review comments

* review comments

* fix comments

* fix comments

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 12:00:00 +02:00
Nuno
2376abb2d3
ref: receive screen layout (#8518)
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
2026-05-03 08:42:51 +02:00
GLaDOS
c063ce81b6
Merge pull request #8515 from BlueWallet/import
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
Update SegmentedControlNativeComponent.ts
2026-05-01 20:42:08 +01:00
Marcos Rodriguez
48e880dbb5 Update SegmentedControlNativeComponent.ts 2026-04-30 11:19:52 -05:00
Overtorment
f75eb7bf71
OPS: Update to RN 85
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-04-29 22:30:57 +01:00
Nuno
03412bbf07
ref: flicker on warning and buttons (#8511) 2026-04-29 22:19:39 +02:00
Marcos Rodriguez
fc05826092 Merge branch 'master' into rn85 2026-04-29 14:02:08 -05:00
GLaDOS
4d662809c2
Merge pull request #8505 from BlueWallet/new-qr
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
feat: new qrcodes
2026-04-29 18:00:11 +01:00
Marcos Rodriguez
00b5129787 OPS: Update to RN 85 2026-04-29 11:30:07 -05:00
Ivan Vershigora
35a39e2153
feat: new qrcodes 2026-04-29 15:10:49 +01:00
GLaDOS
ab01de5ef8
Merge pull request #8507 from BlueWallet/ref-tooltip
REF: tooltip
2026-04-29 14:22:07 +01:00
Overtorment
7b6731b875 REF: tooltip 2026-04-29 12:18:19 +01:00
Overtorment
992e0c987b Merge remote-tracking branch 'origin/master' into ref-tooltip 2026-04-29 12:17:04 +01:00
Nuno
8b3bc67818
fix: image placement on the cards (#8509)
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
2026-04-29 07:13:15 +02:00
Nuno
eaae18d002
fix: send screen scroll and fees modal (#8495) 2026-04-29 07:11:42 +02:00
GLaDOS
9d5af27d02
Merge pull request #8510 from BlueWallet/feat-rounded
feat: Cleaner transaction list
2026-04-29 02:57:26 +01:00
li0nd3v
0594f09a8e fix: lint 2026-04-28 16:30:38 +02:00
li0nd3v
3866b6cd2c fix: lint 2026-04-28 16:11:29 +02:00
li0nd3v
53026182f5 feat: Cleaner transaction list 2026-04-28 16:00:51 +02:00
GLaDOS
8f8c080ed8
Merge pull request #8502 from BlueWallet/segm
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: segmentedcontrol to new arch
2026-04-28 08:28:57 +01:00
Marcos Rodriguez
e098e89dc3 REF: migrate SegmentedControl to New Architecture 2026-04-27 21:20:35 -05:00
overtorment
f1a2d29a1b REF: tooltip 2026-04-27 18:29:41 +01:00
Nuno
038cabedaf
fix: animations got lost (#8504)
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
2026-04-27 16:40:50 +02:00
GLaDOS
f87ffa9633
Merge pull request #8485 from BlueWallet/renovate/actions-cache-digest
Update actions/cache digest to 27d5ce7
2026-04-27 13:49:56 +01:00
GLaDOS
4645ad5c3c
Merge pull request #8500 from BlueWallet/image
feat: drop rn-qr-generator depenedency
2026-04-26 18:25:23 +01:00
Ivan Vershigora
15319ed2e6
feat: drop rn-qr-generator depenedency 2026-04-26 11:28:58 +01:00
GLaDOS
f82de26f59
Merge pull request #8431 from BlueWallet/841
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
OPS: Update to RN 84
2026-04-25 11:55:12 +01:00
Nuno
81170856fb
fix: header with the new arch (#8493) 2026-04-25 09:23:42 +02:00
Overtorment
d28ed3f706
Merge branch 'master' into 841 2026-04-24 19:36:17 +01:00
GLaDOS
2e7785bc60
Merge pull request #8494 from BlueWallet/fix-carousel
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
fix: carousel cards snap
2026-04-24 17:11:13 +01:00
renovate[bot]
1a7b24635a Update dependency react-native-svg to v15.15.4 2026-04-24 16:55:47 +01:00
Ivan
167617d929
Merge branch 'master' into 841 2026-04-24 15:18:07 +01:00
Ivan Vershigora
9cf59da550
fix: use react 19.2.3 2026-04-24 15:15:14 +01:00
Ivan Vershigora
5b1422d049
fix: code format 2026-04-24 14:36:25 +01:00
li0nd3v
b393e97ed5 fix: address comments 2026-04-24 08:31:00 +02:00
Nuno
e5308334c1
Merge branch 'master' into fix-carousel 2026-04-24 08:23:04 +02:00
GLaDOS
3087e8a9ef
Merge pull request #8486 from BlueWallet/renovate/react-native-gesture-handler-2.x
Update dependency react-native-gesture-handler to v2.31.1
2026-04-23 19:54:22 +01:00
GLaDOS
1e4edfafdb
Merge pull request #8496 from BlueWallet/fix-icons
fix: new loading icons
2026-04-23 19:54:15 +01:00
Marcos Rodriguez
a0a42b30dd Merge branch 'master' into 841 2026-04-23 10:55:30 -05:00
Nuno
480bf9f3c5
Merge branch 'master' into fix-icons 2026-04-23 15:31:57 +02:00
li0nd3v
0fd5ec7315 fix: comments 2026-04-23 15:31:29 +02:00
Ivan Vershigora
4f2a1e1f33 fix: podfile.lock update
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
2026-04-23 12:22:31 +01:00
Ojok Emmanuel Nsubuga
af7fc44a3b OPS: Implement 16kb page size support 2026-04-23 12:22:31 +01:00
Nuno
ce4486f80d
Merge pull request #8492 from BlueWallet/fix-static-card 2026-04-22 22:50:26 +02:00
li0nd3v
b055c05f08 fix: remove unnecessary spinner 2026-04-22 19:07:14 +02:00
li0nd3v
2499d0816d fix: icon images 2026-04-22 18:33:33 +02:00
li0nd3v
7073d54089 fix: lint 2026-04-22 17:47:24 +02:00
li0nd3v
c98173471e fix: carousel cards snap 2026-04-22 17:25:18 +02:00
li0nd3v
35d6699085 fix: static card color on home 2026-04-22 16:42:04 +02:00
overtorment
66cf16fba3 FIX: greatly improve startup time
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
2026-04-21 19:18:25 +01:00
overtorment
adbeeaf5f3 fix: bump rn-camera-kit, pin github deps
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
2026-04-21 12:08:26 +01:00
Ivan Vershigora
0caa48d5ed fix: bump rn-camera-kit, pin github deps 2026-04-21 12:08:26 +01:00
Overtorment
2d9d3eb19f ref: use camerakit from github instead of npm 2026-04-21 12:08:26 +01:00
Overtorment
ec9a78a88e REF: bump camerakit 2026-04-21 12:08:26 +01:00
renovate[bot]
6a00db50cb
Update dependency react-native-gesture-handler to v2.31.1 2026-04-15 15:01:29 +00:00
renovate[bot]
f9766ae5fd
Update actions/cache digest to 27d5ce7 2026-04-15 14:57:26 +00:00
GLaDOS
8d5ec224c9
Merge pull request #8472 from BlueWallet/renovate/react-native-vector-icons-material-icons-12.x
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
Update dependency @react-native-vector-icons/material-icons to v12.5.0
2026-04-15 15:55:04 +01:00
GLaDOS
270cafb89a
Merge pull request #8477 from BlueWallet/renovate/androidx.core-core-ktx-1.x
Update dependency androidx.core:core-ktx to v1.18.0
2026-04-15 15:54:53 +01:00
GLaDOS
f3cac05b07
Merge pull request #8464 from BlueWallet/dependabot/npm_and_yarn/lodash-4.18.1
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
Bump lodash from 4.17.21 to 4.18.1
2026-04-10 14:42:14 +01:00
Overtorment
d670c5f029
Merge branch 'master' into dependabot/npm_and_yarn/lodash-4.18.1 2026-04-10 11:02:56 +01:00
GLaDOS
016ea1c9ad
Merge pull request #8479 from BlueWallet/renovate/react-native-gesture-handler-2.x
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
Update dependency react-native-gesture-handler to v2.31.0
2026-04-09 05:19:43 +01:00
renovate[bot]
650973848f
Update dependency react-native-gesture-handler to v2.31.0 2026-04-09 00:53:28 +00:00
GLaDOS
e129791a19
Merge pull request #8478 from BlueWallet/renovate/detox-20.x
Update dependency detox to v20.50.1
2026-04-09 01:47:21 +01:00
renovate[bot]
b8efd854c3
Update dependency detox to v20.50.1 2026-04-08 17:29:46 +00:00
GLaDOS
581ce99f26
Merge pull request #8476 from BlueWallet/renovate/rn-qr-generator-digest
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
Update rn-qr-generator digest to d53be84
2026-04-08 18:24:49 +01:00
renovate[bot]
2d4e1c6d3c
Update dependency androidx.core:core-ktx to v1.18.0 2026-04-08 17:06:33 +00:00
renovate[bot]
6a72414495 Update dependency activesupport to v7.2.3.1 [SECURITY] 2026-04-08 18:04:38 +01:00
renovate[bot]
972886ac7f
Update dependency @react-native-vector-icons/material-icons to v12.5.0 2026-04-08 17:02:31 +00:00
renovate[bot]
9a9c1fcaf0
Update rn-qr-generator digest to d53be84 2026-04-07 09:55:19 +00:00
GLaDOS
58a9cd77b1
Merge pull request #8470 from BlueWallet/renovate/react-native-vector-icons-ionicons-12.x
Some checks failed
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 / buildReleaseApk (push) Has been cancelled
BuildReleaseApk / browserstack (push) Has been cancelled
Update dependency @react-native-vector-icons/ionicons to v12.5.0
2026-04-07 10:47:05 +01:00
renovate[bot]
94fa299c32
Update dependency @react-native-vector-icons/ionicons to v12.5.0 2026-04-07 06:50:00 +00:00
GLaDOS
24d002f2dc
Merge pull request #8471 from BlueWallet/renovate/react-native-vector-icons-material-design-icons-12.x
Update dependency @react-native-vector-icons/material-design-icons to v12.5.0
2026-04-07 07:44:04 +01:00
renovate[bot]
881f07b021
Update dependency @react-native-vector-icons/material-design-icons to v12.5.0 2026-04-06 01:44:41 +00:00
GLaDOS
0b5d9817df
Merge pull request #8453 from BlueWallet/renovate/dayjs-1.x
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
Update dependency dayjs to v1.11.20
2026-04-05 21:49:10 +01:00
GLaDOS
d3dccde0d6
Merge pull request #8457 from BlueWallet/renovate/react-navigation-monorepo
Update react-navigation monorepo
2026-04-05 21:49:04 +01:00
GLaDOS
c04fcf149a
Merge pull request #8463 from BlueWallet/renovate/react-native-vector-icons-entypo-12.x
Update dependency @react-native-vector-icons/entypo to v12.5.0
2026-04-03 02:42:12 +01:00
dependabot[bot]
65ec292138
Bump lodash from 4.17.21 to 4.18.1
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-02 04:16:34 +00:00
renovate[bot]
aa9bd9f545
Update dependency @react-native-vector-icons/entypo to v12.5.0 2026-04-02 02:05:03 +00:00
GLaDOS
77cea7102b
Merge pull request #8454 from BlueWallet/renovate/react-native-gesture-handler-2.x
Update dependency react-native-gesture-handler to v2.30.1
2026-04-02 02:58:21 +01:00
renovate[bot]
c314dcf596
Update react-navigation monorepo 2026-04-01 16:39:07 +00:00
renovate[bot]
f872bc9769
Update dependency react-native-gesture-handler to v2.30.1 2026-04-01 16:38:37 +00:00
renovate[bot]
2b80bbbe2e
Update dependency dayjs to v1.11.20 2026-04-01 16:38:22 +00:00
renovate[bot]
1da5d67959 Pin dependencies 2026-04-01 12:22:30 +01:00
renovate[bot]
c3639a6283 Update maxim-lobanov/setup-xcode digest to ed7a3b1 2026-04-01 12:21:08 +01:00
renovate[bot]
1c6655cc55 Update dependency androidx.compose.ui:ui to v1.10.6 2026-04-01 12:20:32 +01:00
renovate[bot]
839284783b Update actions/cache digest to 6682284 2026-04-01 12:19:39 +01:00
renovate[bot]
24737e5714 Update dependency @babel/preset-env to v7.29.2 2026-04-01 12:16:48 +01:00
renovate[bot]
5813ac9fc5 Update dependency androidx.work:work-runtime-ktx to v2.11.2 2026-04-01 12:13:35 +01:00
GLaDOS
9738edde92
Merge pull request #8406 from BlueWallet/renovate/reactivecircus-android-emulator-runner-digest
Update reactivecircus/android-emulator-runner digest to e89f39f
2026-03-30 15:49:21 +01:00
Overtorment
c935334ca7
Merge branch 'master' into renovate/reactivecircus-android-emulator-runner-digest 2026-03-30 12:31:33 +01:00
Overtorment
24a069fb5d REL: bump to 8.0.0 2026-03-30 11:56:54 +01:00
Nuno
e9d26ac107
Update README.md
Fix translations repo link
2026-03-30 11:05:12 +02:00
Marcos Rodriguez
0b2144b8e0 w9- 2026-03-29 13:32:17 -05:00
Marcos Rodriguez
302f87b881 Merge branch 'master' into 841 2026-03-29 13:30:09 -05:00
Marcos Rodriguez
987449f77c OPS: Fix lint 2026-03-29 13:26:58 -05:00
Overtorment
49cd0c34c4
Merge branch 'master' into renovate/reactivecircus-android-emulator-runner-digest 2026-03-28 22:01:06 +00:00
copilot-swe-agent[bot]
fffbbadd2e REF: Address review comments on TooltipMenu and package.json
Agent-Logs-Url: https://github.com/BlueWallet/BlueWallet/sessions/e7b28207-f39d-474d-9504-12de5332c287

Co-authored-by: marcosrdz <4793122+marcosrdz@users.noreply.github.com>
2026-03-28 21:26:48 +00:00
Marcos Rodriguez
2172d4a20b REF: Update tooltip 2026-03-28 21:26:48 +00:00
Marcos Rodriguez
adc52b53c7 Update TransactionDetails.tsx 2026-03-28 10:30:11 -05:00
Marcos Rodriguez
eed5e77447 Update SettingsButton.tsx 2026-03-27 18:49:28 -05:00
Marcos Rodriguez
d4c2d05808 OPS: Update to RN 84 2026-03-18 17:28:50 -05:00
GLaDOS
c4fefbd7f8
Merge pull request #8425 from BlueWallet/ref-cleanup-andr-build
REF: cleanup and andr build fix
2026-03-18 19:46:37 +00:00
GLaDOS
d2a9280785
Merge pull request #8428 from BlueWallet/cleanup2
fix: old RN code cleanup
2026-03-17 19:53:35 +00:00
GLaDOS
a846cd46ad
Merge pull request #8429 from BlueWallet/locsync30
fix: sync language files
2026-03-17 19:53:31 +00:00
Ivan Vershigora
9ec2cf9b67
fix: sync language files 2026-03-17 16:03:07 +00:00
Ivan Vershigora
08cc14a420
fix: sync language files 2026-03-17 14:08:53 +03:00
Ivan Vershigora
c52c0fd63f
fix: old RN code cleanup 2026-03-17 14:06:01 +03:00
Overtorment
22e6ecbb39 REF: cleanup and andr build fix 2026-03-17 10:40:18 +00:00
Overtorment
ad28079f7c Merge remote-tracking branch 'origin/master' into ref-cleanup-andr-build 2026-03-17 09:58:40 +00:00
GLaDOS
2cc5979f9c
Merge pull request #8426 from BlueWallet/ref-settings-privacy
REF: settings-privacy
2026-03-17 04:53:29 +00:00
Overtorment
5e1c60c5ff REF: settings-privacy 2026-03-16 22:13:55 +00:00
Overtorment
416a643baf REF: cleanup and andr build fix 2026-03-16 21:58:05 +00:00
Overtorment
9dc35cecb9
REF: bump react native 2026-03-16 21:13:23 +00:00
GLaDOS
1d208142e8
Merge pull request #8407 from BlueWallet/renovate/react-native-vector-icons-entypo-12.x
Update dependency @react-native-vector-icons/entypo to v12.4.2
2026-03-14 19:30:33 +00:00
renovate[bot]
c331616a95
Update dependency @react-native-vector-icons/entypo to v12.4.2 2026-03-14 14:33:40 +00:00
renovate[bot]
dd8ec1ce6e
Update reactivecircus/android-emulator-runner digest to e89f39f 2026-03-14 14:26:00 +00:00
GLaDOS
642208d22b
Merge pull request #8401 from BlueWallet/translations_loc-en-json--master_pl
Updates for file loc/en.json in pl
2026-03-10 13:03:21 +00:00
transifex-integration[bot]
e8a7890822
Translate loc/en.json in pl
100% reviewed source file: 'loc/en.json'
on 'pl'.
2026-03-09 16:15:42 +00:00
transifex-integration[bot]
db652eeb32
Translate loc/en.json in pl
100% reviewed source file: 'loc/en.json'
on 'pl'.
2026-03-09 16:15:31 +00:00
GLaDOS
fc37c6f07e
Merge pull request #8382 from BlueWallet/renovate/react-native-tcp-socket-6.x
Update dependency react-native-tcp-socket to v6.4.1
2026-03-06 18:14:41 +00:00
Overtorment
44f497a1cb
Merge branch 'master' into renovate/react-native-tcp-socket-6.x 2026-03-06 16:13:24 +00:00
GLaDOS
7e06e24062
Merge pull request #8384 from BlueWallet/renovate/ruby-3.x
Update dependency ruby to v3.4.8
2026-03-05 14:53:55 +00:00
GLaDOS
5a62ae3fa0
Merge pull request #8387 from BlueWallet/renovate/react-native-permissions-5.x
Update dependency react-native-permissions to v5.5.0
2026-03-05 14:53:51 +00:00
renovate[bot]
26a6cb2c02 Update dependency react-native-safe-area-context to v5.7.0 2026-03-05 13:18:00 +00:00
renovate[bot]
ac2d65ee5a
Update dependency react-native-tcp-socket to v6.4.1 2026-03-05 12:52:37 +00:00
GLaDOS
92ffb5779f
Merge pull request #8386 from BlueWallet/color
Update BlueComponents.tsx
2026-03-05 12:46:05 +00:00
renovate[bot]
ea39e4ee1a
Update dependency react-native-permissions to v5.5.0 2026-03-05 12:42:40 +00:00
renovate[bot]
919dd83d1a Update dependency react-native-keychain to v9.2.3 2026-03-05 12:36:20 +00:00
renovate[bot]
4b9aedaa3a Update dependency react-native-svg to v15.15.3 2026-03-05 12:35:03 +00:00
renovate[bot]
c640591af8
Update dependency ruby to v3.4.8 2026-03-05 12:25:55 +00:00
GLaDOS
030ba4dd8b
Merge pull request #8378 from BlueWallet/ref-electrum-default-server
REF: prefer bluewallet electrum server; drop bad electrum server from…
2026-03-05 12:24:02 +00:00
Marcos Rodriguez
c24a53fc27 Update BlueComponents.tsx 2026-03-04 16:56:31 -05:00
renovate[bot]
a79ca9d17b
Update dependency react-native-share to v12.2.5 (#8380) 2026-03-04 13:46:54 -05:00
overtorment
d7a71bc170 FIX: edit ms cosigner 2026-03-04 13:46:09 -05:00
GLaDOS
93b7bc6234
Merge pull request #8351 from BlueWallet/renovate/detox-20.x
Update dependency detox to v20.47.0
2026-03-04 16:03:34 +00:00
renovate[bot]
91a79addf6
Update dependency detox to v20.47.0 2026-03-04 16:03:15 +00:00
GLaDOS
65387377ec
Merge pull request #8352 from BlueWallet/renovate/fastlane-2.x
Update dependency fastlane to "~> 2.232.0"
2026-03-04 15:44:53 +00:00
GLaDOS
2a5131f278
Merge pull request #8374 from BlueWallet/renovate/react-native-localize-3.x
Update dependency react-native-localize to v3.7.0
2026-03-04 15:44:46 +00:00
GLaDOS
d60efba9b8
Merge pull request #8376 from BlueWallet/renovate/react-native-reanimated-3.x
Update dependency react-native-reanimated to v3.19.5
2026-03-04 15:44:40 +00:00
Ojok Emmanuel Nsubuga
965f1a9f2c ADD: Highlight first and last segments of bitcoin address 2026-03-04 10:32:57 -05:00
Marcos Rodriguez
023ca31536 FIX: JSX issue 2026-03-04 10:30:39 -05:00
Overtorment
282cd6cfd7
Delete tests/unit/blue-electrum-peer-rotation.test.js 2026-03-04 13:45:07 +00:00
Cursor Agent
dab363dbd7 FIX: rotate through all hardcoded electrum peers
Co-authored-by: Overtorment <Overtorment@users.noreply.github.com>
2026-03-04 13:18:29 +00:00
overtorment
2bfcc75882 REF: prefer bluewallet electrum server; drop bad electrum server from the list 2026-03-04 13:06:22 +00:00
renovate[bot]
3e21bfcfa4
Update dependency react-native-reanimated to v3.19.5 2026-03-04 12:56:16 +00:00
GLaDOS
5240fc4e0d
Merge pull request #8373 from BlueWallet/renovate/actions-setup-node-digest
Update actions/setup-node digest to 53b8394
2026-03-04 12:44:35 +00:00
GLaDOS
e60ddbc071
Merge pull request #8366 from BlueWallet/renovate/react-native-vector-icons-ionicons-12.x
Update dependency @react-native-vector-icons/ionicons to v12.4.1
2026-03-04 10:58:18 +00:00
renovate[bot]
98a6becf32
Update dependency react-native-localize to v3.7.0 2026-03-04 10:56:02 +00:00
renovate[bot]
cfbb01571e
Update actions/setup-node digest to 53b8394 2026-03-04 10:43:23 +00:00
GLaDOS
a1abd15a5e
Merge pull request #8356 from BlueWallet/renovate/react-native-vector-icons-entypo-12.x
Update dependency @react-native-vector-icons/entypo to v12.4.1
2026-03-03 20:42:41 +00:00
Marcos Rodriguez
844f01b42a OPS: Podfile 2026-03-03 15:29:45 -05:00
GLaDOS
e5670e99de
Merge pull request #8365 from BlueWallet/renovate/react-native-vector-icons-material-icons-12.x
Update dependency @react-native-vector-icons/material-icons to v12.4.1
2026-03-03 18:11:32 +00:00
renovate[bot]
1b7a27839c
Update dependency @react-native-vector-icons/ionicons to v12.4.1 2026-03-03 18:01:26 +00:00
renovate[bot]
82b7db8f48
Update dependency @react-native-vector-icons/entypo to v12.4.1 2026-03-03 18:01:15 +00:00
GLaDOS
af4d7556c6
Merge pull request #8354 from BlueWallet/renovate/react-native-edge-to-edge-1.x
Update dependency react-native-edge-to-edge to v1.8.1
2026-03-03 17:53:16 +00:00
GLaDOS
58c4544a50
Merge pull request #8355 from BlueWallet/renovate/react-native-gesture-handler-2.x
Update dependency react-native-gesture-handler to v2.30.0
2026-03-03 17:53:13 +00:00
GLaDOS
48e00dd57b
Merge pull request #8357 from BlueWallet/renovate/react-native-vector-icons-fontawesome-12.x
Update dependency @react-native-vector-icons/fontawesome to v12.4.1
2026-03-03 17:53:07 +00:00
Marcos Rodriguez Vélez
3e8906e31b
Merge pull request #8362 from BlueWallet/enhance-address 2026-03-03 12:31:06 -05:00
Marcos Rodriguez Vélez
e64d805f07
Merge pull request #8361 from BlueWallet/taptocopy 2026-03-03 12:30:01 -05:00
Marcos Rodriguez
50e4e3a581 FIX: Icon alignment 2026-03-03 12:29:34 -05:00
renovate[bot]
8382fd601e
Update dependency @react-native-vector-icons/material-icons to v12.4.1 2026-03-03 16:10:52 +00:00
GLaDOS
f631ef37b9
Merge pull request #8364 from BlueWallet/renovate/react-native-vector-icons-material-design-icons-12.x
Update dependency @react-native-vector-icons/material-design-icons to v12.4.1
2026-03-03 16:03:06 +00:00
Ojok Emmanuel Nsubuga
e1b8fd91a8 ADD: Highlight first and last segments of bitcoin address 2026-03-03 15:02:27 +03:00
renovate[bot]
70aadbe9ba
Update dependency @react-native-vector-icons/material-design-icons to v12.4.1 2026-03-03 11:47:08 +00:00
renovate[bot]
8355954b9b
Update dependency @react-native-vector-icons/fontawesome to v12.4.1 2026-03-03 11:47:00 +00:00
GLaDOS
69a0569998
Merge pull request #8363 from BlueWallet/renovate/react-native-vector-icons-fontawesome6-12.x
Update dependency @react-native-vector-icons/fontawesome6 to v12.3.1
2026-03-03 11:39:53 +00:00
renovate[bot]
22853c1079
Update dependency @react-native-vector-icons/fontawesome6 to v12.3.1 2026-03-02 21:59:27 +00:00
GLaDOS
638254edce
Merge pull request #8349 from BlueWallet/renovate/androidx.compose.ui-ui-1.x
Update dependency androidx.compose.ui:ui to v1.10.4
2026-03-02 21:51:29 +00:00
Marcos Rodriguez
077fddf75f ADD: Tap to copy missing in tx details 2026-03-02 11:34:17 -05:00
Overtorment
baf9f17231
OPS: renovate config 2026-03-02 16:08:35 +00:00
renovate[bot]
f9413a8c1b
Update dependency react-native-gesture-handler to v2.30.0 2026-03-02 15:08:15 +00:00
renovate[bot]
ee05946c2a
Update dependency react-native-edge-to-edge to v1.8.1 2026-03-02 15:08:00 +00:00
renovate[bot]
f04e2afe5e
Update dependency fastlane to "~> 2.232.0" 2026-03-02 15:07:38 +00:00
renovate[bot]
362cff7783
Update dependency androidx.compose.ui:ui to v1.10.4 2026-03-02 15:06:52 +00:00
Marcos Rodriguez Vélez
37916ab2e5
REF: Use new icons package 2026-03-02 14:54:47 +00:00
GLaDOS
ae761a7ea8
Merge pull request #8350 from BlueWallet/renovate/androidx.work-work-runtime-ktx-2.x
Update dependency androidx.work:work-runtime-ktx to v2.11.1
2026-03-02 13:01:09 +00:00
renovate[bot]
61bb1823f7
Update dependency androidx.work:work-runtime-ktx to v2.11.1 2026-03-02 11:18:13 +00:00
GLaDOS
abbc780323
Merge pull request #8344 from BlueWallet/translations_loc-en-json--master_pl
Updates for file loc/en.json in pl
2026-03-02 11:06:04 +00:00
transifex-integration[bot]
511c3c4c66
Translate loc/en.json in pl
100% reviewed source file: 'loc/en.json'
on 'pl'.
2026-03-01 20:26:59 +00:00
GLaDOS
16aacd4bbb
Merge pull request #8341 from BlueWallet/renovate/react-native-community-push-notification-ios-1.x
Update dependency @react-native-community/push-notification-ios to v1.12.0
2026-03-01 17:13:16 +00:00
GLaDOS
c60839445d
Merge pull request #8338 from BlueWallet/renovate/react-navigation-monorepo
Update react-navigation monorepo
2026-03-01 17:00:58 +00:00
GLaDOS
2b2a40f22d
Merge pull request #8340 from BlueWallet/renovate/bugsnag-js-monorepo
Update dependency @bugsnag/react-native to v8.8.1
2026-03-01 17:00:54 +00:00
renovate[bot]
adf20d84d3
Update dependency @react-native-community/push-notification-ios to v1.12.0 2026-03-01 09:15:35 +00:00
renovate[bot]
020177c30a
Update dependency @bugsnag/react-native to v8.8.1 2026-03-01 09:15:24 +00:00
renovate[bot]
d436ae7db2
Update react-navigation monorepo 2026-03-01 09:15:09 +00:00
Overtorment
3e280edf1b
OPS: ios build fix (#8339) 2026-02-28 18:55:22 -05:00
renovate[bot]
2b1dd08716 Pin dependencies 2026-02-27 16:06:26 +00:00
renovate[bot]
8db841add2 Update dependency @babel/preset-env to v7.29.0 2026-02-27 13:41:25 +00:00
GLaDOS
882717e775
Merge pull request #8331 from BlueWallet/renovate/bip32-5.x
Update dependency bip32 to v5.0.1
2026-02-27 01:25:06 +00:00
renovate[bot]
3d1c5d7466
Update dependency bip32 to v5.0.1 2026-02-26 19:14:29 +00:00
Overtorment
d1c457c6ae
OPS: renovate config 2026-02-26 19:03:26 +00:00
GLaDOS
2315ea7fdf
Merge pull request #8305 from BlueWallet/e2e
Improve e2e test DX: cleaner stack traces & robust emulator setup
2026-02-26 16:52:55 +00:00
Overtorment
01e4b37348
Merge branch 'master' into e2e 2026-02-26 14:48:31 +00:00
overtorment
0d1f09408b FIX: import keystone wallets 2026-02-26 14:45:29 +00:00
Adam SHaY
8bb4ee916c FIX: maxSendableAmount using wrong balance 2026-02-26 14:34:32 +00:00
Adam SHaY
4ad7cfe6f0 REF: move maxSendableAmount calc into fee useEffect 2026-02-26 14:34:32 +00:00
Adam SHaY
e646bdaee3 FIX: align max sendable text to center 2026-02-26 14:34:32 +00:00
Adam SHaY
0f8b04d43a ADD: show MAX sendable amount
When MAX is selected, display the calculated sendable amount below.
  - Works only for 1 recipient (Main goal of feature is this for atomic swap)
  - Shows ≈ prefix when recipient address is unknown (using P2TR estimate)
  - Shows exact amount when valid address is entered
  - Forces displayed amount into transaction (no recalculation on Next)
2026-02-26 14:34:32 +00:00
GLaDOS
248182c1f6
Merge pull request #8309 from BlueWallet/cccc
OPS: Catalyst
2026-02-26 01:21:35 +00:00
Marcos Rodriguez
f70685b769 Merge branch 'master' into cccc 2026-02-25 18:12:01 -05:00
Marcos Rodriguez Vélez
d5c56b6e69 add: additional unique ID instructions in issue template 2026-02-25 10:34:12 +00:00
Marcos Rodriguez
2956f2e5a5 ADD: integrate match for catalyst provisioning profiles with manual signing 2026-02-24 18:15:36 -05:00
Marcos Rodriguez
019f93d7c1 Merge branch 'master' into cccc 2026-02-24 18:01:08 -05:00
Marcos Rodriguez
403573a8d3 OPS: version bump 2026-02-23 18:01:08 -05:00
Marcos Rodriguez
a6358e86af wip 2026-02-23 13:25:04 +00:00
overtorment
6f422d4299 FIX: startup crash because of bugsnag 2026-02-23 13:25:04 +00:00
Marcos Rodriguez
d13922165c OPS: Version bump 2026-02-23 06:52:53 -05:00
Marcos Rodriguez
64414e40d3 ADD: re-run catalyst build on push while mac-dmg label is present 2026-02-22 17:16:30 -05:00
Marcos Rodriguez
feecc46490 Update build-mac-catalyst.yml 2026-02-22 15:01:51 -05:00
Marcos Rodriguez Vélez
207f5b549f
Merge pull request #8322 from BlueWallet/copilot/sub-pr-8309 2026-02-22 14:09:07 -05:00
copilot-swe-agent[bot]
5280b7b521 Address review comments: fix shell escaping, API key creation, lane description
Co-authored-by: marcosrdz <4793122+marcosrdz@users.noreply.github.com>
2026-02-22 19:05:56 +00:00
Marcos Rodriguez Vélez
b3b635ef00
Merge pull request #8324 from BlueWallet/copilot/sub-pr-8309-another-one 2026-02-22 14:02:34 -05:00
Marcos Rodriguez Vélez
da3767266f
Merge pull request #8323 from BlueWallet/copilot/sub-pr-8309-again 2026-02-22 13:57:27 -05:00
copilot-swe-agent[bot]
0ae944abbc FIX: use array form of sh and File.open to avoid shell injection in Fastfile
Co-authored-by: marcosrdz <4793122+marcosrdz@users.noreply.github.com>
2026-02-22 18:56:30 +00:00
copilot-swe-agent[bot]
9c8f660604 Add step to create appstore_api_key.json before TestFlight upload
Co-authored-by: marcosrdz <4793122+marcosrdz@users.noreply.github.com>
2026-02-22 18:54:14 +00:00
copilot-swe-agent[bot]
0cf466910f Initial plan 2026-02-22 18:51:45 +00:00
copilot-swe-agent[bot]
e991ed8814 Initial plan 2026-02-22 18:51:31 +00:00
copilot-swe-agent[bot]
849191f413 Initial plan 2026-02-22 18:50:37 +00:00
Marcos Rodriguez Vélez
a066d6c377
Update Fastfile
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-22 13:49:45 -05:00
Marcos Rodriguez Vélez
4b240b429b
Update build-mac-catalyst.yml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-22 13:49:38 -05:00
Marcos Rodriguez Vélez
1386e0738a
Update build-mac-catalyst.yml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-22 13:49:18 -05:00
Marcos Rodriguez Vélez
f390d2015b
Update Fastfile
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-22 13:48:57 -05:00
Marcos Rodriguez Vélez
c3fa43ccb8
Update build-mac-catalyst.yml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-22 13:48:46 -05:00
Marcos Rodriguez Vélez
70dc1e2f5a
Merge branch 'master' into cccc 2026-02-22 13:32:14 -05:00
Marcos Rodriguez Vélez
2b9dbc0a28
Update Fastfile 2026-02-22 13:31:25 -05:00
Marcos Rodriguez Vélez
4dae36b251
Update build-mac-catalyst.yml 2026-02-22 13:29:33 -05:00
overtorment
f5233191af FIX: graceful error handling (attempt to prevent startup crashes) 2026-02-22 15:27:54 +00:00
Marcos Rodriguez
19c0d3ae39 ADD: upload Mac Catalyst to TestFlight when both mac-dmg and testflight labels present 2026-02-21 20:32:08 -05:00
Marcos Rodriguez
582d6b8e44 Update Fastfile 2026-02-21 20:29:23 -05:00
Marcos Rodriguez
b77a9b27ca ADD: multilingual README in DMG (EN, ES, ZH, PT, RU, JA, DE, FR, AR) 2026-02-21 20:26:36 -05:00
Marcos Rodriguez
93faa47121 ADD: trigger catalyst build only on mac-dmg label, cancel on unlabel 2026-02-21 20:25:48 -05:00
Marcos Rodriguez
bfe73af235 ADD: post PR comment with direct DMG download link 2026-02-21 20:25:01 -05:00
Marcos Rodriguez
fb1a25dba7 Merge branch 'master' into cccc 2026-02-21 20:23:38 -05:00
Marcos Rodriguez
e7a607b6bb ADD: README with first-launch instructions in Catalyst DMG 2026-02-21 20:23:25 -05:00
GLaDOS
9c27fff4a9
Merge pull request #8313 from BlueWallet/translations_loc-en-json--master_zh_CN
Updates for file loc/en.json in zh_CN
2026-02-21 20:22:36 +00:00
transifex-integration[bot]
d4c79c7f9b
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2026-02-21 13:08:40 +00:00
Marcos Rodriguez
0cc60dc1a6 OPS: version bump 2026-02-20 22:32:32 -05:00
GLaDOS
a26237162a
Merge pull request #8311 from BlueWallet/prop
OPS: Update package
2026-02-20 20:13:24 +00:00
Marcos Rodriguez
fa7420ea18 fix: use of usescreenprotect 2026-02-20 11:21:37 -05:00
Marcos Rodriguez
e729f74a1e OPS: Update package 2026-02-20 10:39:44 -05:00
Marcos Rodriguez
c36f1f934f Update project.pbxproj 2026-02-19 21:16:26 -05:00
Marcos Rodriguez
66b3e261e5 FIX: disable xcodebuild code signing when no credentials provided 2026-02-19 21:13:12 -05:00
Marcos Rodriguez
67a5f2c9ac ADD: Mac Catalyst build lane with DMG, ad-hoc signing, no notarization 2026-02-19 20:50:19 -05:00
Marcos Rodriguez
6078d47d40 FIX: skip notarization when secrets are empty strings 2026-02-19 19:13:43 -05:00
Marcos Rodriguez
b33ea3e1cc Merge branch 'cccc' of https://github.com/BlueWallet/BlueWallet into cccc 2026-02-19 15:05:59 -05:00
Marcos Rodriguez
8044d37666 Merge branch 'cccc' of https://github.com/BlueWallet/BlueWallet into cccc 2026-02-19 15:05:15 -05:00
Marcos Rodriguez
c52742e1b6 Merge branch 'cccc' of https://github.com/BlueWallet/BlueWallet into cccc 2026-02-19 15:03:17 -05:00
Marcos Rodriguez
44602e406e FIX: Catalyst build - arm64 only, auth bypass, robust app path detection 2026-02-19 15:02:35 -05:00
Marcos Rodriguez
8ea9b7aa16 FIX: Catalyst build - arm64 only, auth bypass, robust app path detection 2026-02-19 00:08:09 -05:00
GLaDOS
22f356f29a
Merge pull request #8308 from BlueWallet/fee-selector
fix: fee modal not aligned to bottom and keyboard
2026-02-19 00:50:22 +00:00
Marcos Rodriguez
0905c28989 OPS: Catalyst 2026-02-18 18:46:44 -05:00
GLaDOS
f433968b36
Merge pull request #8307 from BlueWallet/dd
fix: styles
2026-02-18 14:19:55 +00:00
Ivan Vershigora
7f46fee344
fix: prettier 2026-02-18 12:08:28 +00:00
Nuno
6a2293c7d4
Merge branch 'master' into fee-selector 2026-02-18 12:51:29 +01:00
Nuno
d0ed3d2e44
Merge branch 'master' into dd 2026-02-18 12:51:22 +01:00
Nuno
5c6f4cb64d
Merge pull request #8303 from BlueWallet/fix-dependency 2026-02-18 11:29:03 +01:00
Li0nd3v
54b9c2dd1d fix: fee modal not aligned to bottom and keyboard 2026-02-18 11:01:52 +01:00
Li0nd3v
0e58e9ab63 fix lint 2026-02-18 10:30:37 +01:00
Li0nd3v
2d38bc0424 fix more styles 2026-02-18 10:24:38 +01:00
Marcos Rodriguez
bfbbb32ff1 fix: styles 2026-02-17 23:10:20 -05:00
GLaDOS
973d10081e
Merge pull request #8306 from BlueWallet/widthj
FIX: Scan button layout
2026-02-17 19:57:11 +00:00
Marcos Rodriguez
6257697ae9 FIX: Scan button layout 2026-02-17 12:36:30 -05:00
Ojok Emmanuel Nsubuga
a12d60c967 FIX: gradle build error 2026-02-17 16:13:53 +03:00
Ivan Vershigora
a3a10ff440
feat: e2e stacktrace; emulator prepare script 2026-02-17 12:33:00 +00:00
Ojok Emmanuel Nsubuga
476d822ec4 FIX: resolve react-native-capture-protection to correct commit hash 2026-02-17 11:55:45 +03:00
Marcos Rodriguez
349bca7617 Update generateWord.tsx 2026-02-16 11:44:37 -05:00
Marcos Rodriguez
7a0da56fd1 fix: lint 2026-02-16 11:43:47 -05:00
Marcos Rodriguez
f07584d630 Revert "ops: update RN"
This reverts commit a5040adb64.
2026-02-16 11:36:06 -05:00
Marcos Rodriguez
e54580da6e colors 2026-02-15 17:09:06 -05:00
Marcos Rodriguez
a5040adb64 ops: update RN 2026-02-15 17:04:15 -05:00
Marcos Rodriguez
fe6caaf3e0 Update index.tsx 2026-02-15 16:54:53 -05:00
Marcos Rodriguez
6c2e6dc29e Update Podfile.lock 2026-02-15 16:52:09 -05:00
GLaDOS
1cf76793fc
Merge pull request #8300 from BlueWallet/fix-rate-app
FIX: rate app button
2026-02-15 12:21:23 +00:00
Overtorment
9213491a0f FIX: rate app button 2026-02-14 22:07:05 +00:00
GLaDOS
6a07a8c33b
Merge pull request #8299 from BlueWallet/translations_loc-en-json--master_es_419
Updates for file loc/en.json in es_419
2026-02-14 03:16:15 +00:00
transifex-integration[bot]
c4324a5079
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2026-02-13 22:31:16 +00:00
GLaDOS
ba47903e9c
Merge pull request #8294 from BlueWallet/translations_loc-en-json--master_zh_CN
Updates for file loc/en.json in zh_CN
2026-02-13 13:34:15 +00:00
transifex-integration[bot]
672aece2b1
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2026-02-13 08:42:22 +00:00
Marcos Rodriguez Vélez
557b263935
Merge pull request #8290 from BlueWallet/tx-list
ref: transaction list with date and title
2026-02-12 21:36:38 -05:00
Marcos Rodriguez
5dc6186d4d fix lint 2026-02-12 21:30:35 -05:00
Li0nd3v
bf46d85ba6 fix timestamp and layout on txlist 2026-02-11 11:59:12 +01:00
Nuno
2284b30740
Merge branch 'master' into tx-list 2026-02-11 11:58:15 +01:00
GLaDOS
95ef556204
Merge pull request #8291 from BlueWallet/react-native-edge-to-edge
fix: add react-native-edge-to-edge
2026-02-10 16:41:32 +00:00
Ivan Vershigora
8e55e5ee75
fix: add react-native-edge-to-edge 2026-02-10 13:02:00 +00:00
Overtorment
9a08af4784 ADD: native ark-to-ark transfers 2026-02-10 09:51:43 +00:00
Overtorment
d9f6f7bba0 FIX: multisig cosigner edit 2026-02-09 19:58:40 +00:00
GLaDOS
e086f04b4d
Merge pull request #8287 from BlueWallet/ref-cleanup-old-stuff
FIX: cleanup
2026-02-09 19:31:06 +00:00
Li0nd3v
80cdec5ab2 ref: add memo back 2026-02-09 18:47:13 +01:00
Li0nd3v
b25c687822 fix date format 2026-02-09 18:10:52 +01:00
Li0nd3v
b41be54c61 ref: transaction list with date and title 2026-02-09 18:06:52 +01:00
Overtorment
3a69c61635 REF: cleanup 2026-02-08 12:49:58 +00:00
Adam SHaY
0b1086e73b fix: add close button on send LN transaction 2026-02-06 20:54:55 +00:00
Adam SHaY
db6c2b9ff7 FIX: Show loading indicator during wallet discovery 2026-02-06 20:44:13 +00:00
Marcos Rodriguez
98aa9099b7 FIX: Bugsnag reporting was not working 2026-02-06 11:01:08 +00:00
Ojok Emmanuel Nsubuga
2fe0b6a2ad FIX: Make BBQR work with multisig 2026-02-06 11:00:28 +00:00
GLaDOS
5618f43a9f
Merge pull request #8272 from BlueWallet/translations_loc-en-json--master_es_419
Updates for file loc/en.json in es_419
2026-02-03 13:45:25 +00:00
transifex-integration[bot]
fa55993518
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2026-02-03 10:18:53 +00:00
GLaDOS
091477a708
Merge pull request #8270 from BlueWallet/lockf
Delete lockfiles_update.yml
2026-02-02 11:53:32 +00:00
Marcos Rodriguez
bba4df4db6 Delete lockfiles_update.yml 2026-02-01 20:02:45 -05:00
Marcos Rodriguez Vélez
22c4368be9
Merge pull request #8267 from BlueWallet/navscrerens
DEL: Remove redundant nav stack files
2026-02-01 19:47:22 -05:00
Marcos Rodriguez
02882751c0 Update UnlockWith.tsx 2026-02-01 09:52:48 -05:00
Marcos Rodriguez Vélez
7696011916
Potential fix for pull request finding 'Unused variable, import, function or class'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-02-01 01:22:05 -05:00
Marcos Rodriguez
bb1e3e9f66 Update UnlockWith.tsx 2026-01-31 23:01:41 -05:00
Marcos Rodriguez
b0322d99cc Update UnlockWith.tsx 2026-01-31 22:52:26 -05:00
Marcos Rodriguez
a4bbbc95d4 DEL: Remove redundant nav stack files 2026-01-31 16:17:33 -05:00
GLaDOS
61dcb84483
Merge pull request #8263 from BlueWallet/translations_loc-en-json--master_zh_CN
Updates for file loc/en.json in zh_CN
2026-01-30 14:02:31 +00:00
GLaDOS
469c0fd4e2
Merge pull request #8254 from BlueWallet/renovate/rn-qr-generator-digest
Update rn-qr-generator digest to 5870927
2026-01-30 10:57:04 +00:00
transifex-integration[bot]
6afdcc24c9
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2026-01-29 00:48:05 +00:00
Marcos Rodriguez
b2eb081f39 ops: fix package 2026-01-28 11:29:02 -05:00
Marcos Rodriguez
ca6e3bb354 Update package-lock.json 2026-01-28 11:07:32 -05:00
Marcos Rodriguez
8c60cc0ed7 ff 2026-01-27 21:54:13 -05:00
Marcos Rodriguez Vélez
c9584da872
Merge pull request #8249 from BlueWallet/sertingsplatform 2026-01-26 14:18:12 -05:00
Ivan Vershigora
b94a3de2b1 fix: simplify settings screens 2026-01-26 12:28:58 +00:00
Marcos Rodriguez
852461f62c ref: reduce code for settings screens 2026-01-26 12:28:58 +00:00
renovate[bot]
11405f07ef
Update rn-qr-generator digest to 5870927 2026-01-25 16:07:03 +00:00
GLaDOS
8ad84cb087
Merge pull request #8260 from BlueWallet/ops-update-tests-after-compromised-test-seed
OPS: update tests after compromised test seed
2026-01-25 13:00:06 +00:00
overtorment
df5f1dd9c1 OPS: update tests after compromised test seed 2026-01-25 10:58:11 +00:00
GLaDOS
0e1430fd5c
Merge pull request #8240 from BlueWallet/add-bbqr-2
ADD: psbt signing via bbqr (closes #6311)
2026-01-19 15:56:03 +00:00
GLaDOS
e1c1f57964
Merge pull request #8250 from BlueWallet/nickfarrow-master
Nickfarrow master
2026-01-16 18:15:48 +00:00
overtorment
f4245109bb REF: cleanup and refactor of wallet descriptor detection 2026-01-16 13:24:33 +00:00
overtorment
e81e7cd21b Merge branch 'master' of github.com:nickfarrow/BlueWallet into nickfarrow-master 2026-01-16 12:35:19 +00:00
overtorment
acad1e487d REF: resolved conflict 2026-01-16 12:16:22 +00:00
Marcos Rodriguez Vélez
fba1556996
Update fiatUnits.json 2026-01-15 22:43:58 -05:00
Ivan Vershigora
d7fcea8b4f ref: replace Buffer with Uint8Array in some internal structures 2026-01-15 13:20:42 +00:00
pechen987
5911e2f373 ADD: display electrum server banner in settings 2026-01-15 12:20:26 +00:00
Nick Farrow
cc79e9809d
FIX: wrong address type for descriptors with custom paths 2026-01-15 06:34:57 +11:00
GLaDOS
8edc81f9ee
Merge pull request #8221 from BlueWallet/t2t
ref: improve stability of e2e tests
2026-01-14 13:13:10 +00:00
Ivan Vershigora
fa3eb56418
ref: improve stability of e2e tests 2026-01-14 09:07:42 +00:00
GLaDOS
2f52312388
Merge pull request #8243 from BlueWallet/ref-utils
REF: move all utils to the same dir
2026-01-13 03:17:20 +00:00
GLaDOS
ff7015ccda
Merge pull request #8242 from BlueWallet/locsync29
feat: new languages
2026-01-12 23:45:04 +00:00
overtorment
80fcac6148 REF: move all utils to the same dir 2026-01-12 19:53:46 +00:00
Ivan Vershigora
db518c9743
feat: new languages 2026-01-12 19:27:06 +00:00
GLaDOS
82f39579dc
Merge pull request #8241 from BlueWallet/translations_loc-en-json--master_es_419
Updates for file loc/en.json in es_419
2026-01-12 19:14:16 +00:00
transifex-integration[bot]
b7893d7a0f
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2026-01-12 13:08:31 +00:00
overtorment
d08f8f661e ADD: psbt signing via bbqr (closes #6311) 2026-01-11 21:40:14 +00:00
Marcos Rodriguez
82388e76c4 fix: style 2026-01-10 16:45:59 -05:00
overtorment
17a55ee7cb ADD: pair with coldcard q via bbqr 2026-01-10 20:52:10 +00:00
overtorment
8a320744a6 ADD: pair with coldcard q via bbqr 2026-01-10 20:52:10 +00:00
overtorment
08d2b26067 ADD: pair with coldcard q via bbqr 2026-01-10 20:52:10 +00:00
overtorment
d5bd04c1ed ADD: pair with coldcard q via bbqr 2026-01-10 20:52:10 +00:00
GLaDOS
97ffdfae36
Merge pull request #8167 from BlueWallet/renovate/react-native-qrcode-svg-6.x
Update dependency react-native-qrcode-svg to v6.3.21
2026-01-10 20:35:04 +00:00
GLaDOS
3bfe228dd1
Merge pull request #8237 from BlueWallet/fix
fix: run prettier
2026-01-10 20:34:56 +00:00
Ivan Vershigora
dde1022d1f
fix: run prettier 2026-01-10 17:31:05 +00:00
GLaDOS
edcd9c3226
Merge pull request #8235 from BlueWallet/translations_loc-en-json--master_zh_CN
Updates for file loc/en.json in zh_CN
2026-01-10 16:50:25 +00:00
GLaDOS
34bded5496
Merge pull request #8229 from BlueWallet/v24
fix: bump node to v24, add claude.md
2026-01-10 13:56:36 +00:00
transifex-integration[bot]
3eafc34691
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2026-01-10 08:15:31 +00:00
Nuno
df5e0c039f
REF: update settings screens with new theme system 2026-01-09 19:53:28 +00:00
GLaDOS
84beb37f08
Merge pull request #8230 from BlueWallet/eci
fix: bump camera to v16.2.0 with qrcode eci support
2026-01-08 15:54:38 +00:00
Ojok Emmanuel Nsubuga
c1b15bedd2 REF: Update CosignerEntry type 2026-01-08 14:26:24 +00:00
Ojok Emmanuel Nsubuga
be91ee9913 REF: Convert to ts 2026-01-08 14:26:24 +00:00
Ivan Vershigora
797c8d2e46
fix: bump camera to v16.2.0 with qrcode eci support 2026-01-08 09:16:04 +00:00
GLaDOS
7f11e118c0
Merge pull request #8227 from BlueWallet/ref-bump-arkade
REF: bump arkade
2026-01-07 15:11:11 +00:00
Ivan Vershigora
588315e1d6 feat: new camera without google mlkit 2026-01-07 10:51:58 +00:00
overtorment
84b4525034 REF: bump arkade 2026-01-07 10:19:04 +00:00
Ivan Vershigora
3d1d173ec4
fix: bump node to v24 2026-01-06 15:51:05 +00:00
overtorment
040791737c REF: bump arkade 2026-01-06 11:32:57 +00:00
renovate[bot]
35a40eb96a
Update dependency react-native-qrcode-svg to v6.3.21 2026-01-04 23:57:37 +00:00
Ojok Emmanuel Nsubuga
d59cf0f200 REF: Update psbt type 2025-12-22 20:23:50 +00:00
Ojok Emmanuel Nsubuga
a2ea14a666 FIX: Navigation and loading state error raised by cursor bot 2025-12-22 20:23:50 +00:00
Ojok Emmanuel Nsubuga
f8b81dcbbe REF: Convert psbtWithHardwareWallet to ts 2025-12-22 20:23:50 +00:00
overtorment
accb1a15db FIX: import psbt files 2025-12-20 21:28:52 +00:00
Marcos Rodriguez
5cf8f0dd83 FIX: Import PSBT
Should be good for now as the API is updated to the new specs
2025-12-19 21:13:24 +00:00
Ivan Vershigora
9d1b6e3a7d feat: detox ios tests 2025-12-16 10:20:42 +00:00
IgboPharaoh
97b6607bef FIX: button padding on onchain receive with amount screen 2025-12-16 10:19:30 +00:00
IgboPharaoh
79164ca325 FIX: button padding on onchain receive with amount screen 2025-12-16 10:19:30 +00:00
GLaDOS
cd008504de
Merge pull request #8211 from BlueWallet/ref-gradients
REF: wallets gradients
2025-12-15 18:02:04 +00:00
overtorment
a9550959a7 REF: wallets gradients 2025-12-15 16:53:14 +00:00
dependabot[bot]
28d23b1ae4 Bump sha.js from 2.4.11 to 2.4.12
Bumps [sha.js](https://github.com/crypto-browserify/sha.js) from 2.4.11 to 2.4.12.
- [Changelog](https://github.com/browserify/sha.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/sha.js/compare/v2.4.11...v2.4.12)

---
updated-dependencies:
- dependency-name: sha.js
  dependency-version: 2.4.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-14 19:46:46 +00:00
dependabot[bot]
266eda017d Build(deps): Bump node-forge from 1.3.1 to 1.3.3
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.3.1 to 1.3.3.
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.3.3)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-version: 1.3.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-14 19:44:40 +00:00
Marcos Rodriguez Vélez
097b3758ce
Merge pull request #8208 from BlueWallet/delw
DEL: Remove from Manage Wallets
2025-12-12 17:30:14 -05:00
Marcos Rodriguez
03bdc1a1d6 Update ManageWallets.tsx 2025-12-12 13:23:07 -05:00
Marcos Rodriguez
931d901ffe DEL: Remove from Manage Wallets 2025-12-12 12:55:13 -05:00
GLaDOS
2ea4424b03
Merge pull request #8206 from BlueWallet/x64
feat: only build x64 android for github actions
2025-12-10 21:05:26 +00:00
Ivan Vershigora
a633c25420
feat: only build x64 android for github actions 2025-12-10 21:05:37 +04:00
GLaDOS
d74269a413
Merge pull request #8199 from BlueWallet/ref-more-ts-conversion-3
Ref more ts conversion 3
2025-12-10 16:43:56 +00:00
GLaDOS
ae849f7fa5
Merge pull request #8205 from BlueWallet/ref-arkade
REF: bump ark and minor improvements
2025-12-10 06:49:57 +00:00
GLaDOS
c45f125496
Merge pull request #8193 from BlueWallet/translations_loc-en-json--master_zh_CN
Updates for file loc/en.json in zh_CN
2025-12-09 21:58:54 +00:00
Overtorment
d7abf4672f
Merge branch 'master' into translations_loc-en-json--master_zh_CN 2025-12-09 20:56:09 +00:00
overtorment
b8d8404582 TST: fix e2e 2025-12-09 20:55:34 +00:00
overtorment
2a774b8289 REF: bump ark and minor improvements 2025-12-09 20:46:20 +00:00
overtorment
186ae0b865 REF: bump ark and minor improvements 2025-12-09 20:26:57 +00:00
overtorment
f36b0c5e91 REF: sign-verify screen to ts 2025-12-08 16:08:29 +00:00
overtorment
582d8aa613 REF: generateWord to ts 2025-12-08 16:08:09 +00:00
overtorment
70facab01b Merge remote-tracking branch 'origin/master' into ref-more-ts-conversion-3 2025-12-08 15:44:24 +00:00
overtorment
5258c97680 REF: ln create invoice to ts 2025-12-08 15:16:04 +00:00
overtorment
d9fcf50829 REF: ln view preimage to ts 2025-12-08 15:16:04 +00:00
overtorment
207e8dc03d REF: ln create invoice to ts 2025-12-08 15:16:04 +00:00
overtorment
09ebf3c1ba REF: backup ln to ts 2025-12-08 15:16:04 +00:00
overtorment
ac72d27fa6 REF: lnurl auth to ts 2025-12-08 15:16:04 +00:00
Marcos Rodriguez
615981bb5e OPS: Version bump 2025-12-07 13:52:53 -05:00
overtorment
46fa6ca9ba REF: generateWord to ts 2025-12-06 18:57:18 +00:00
overtorment
96433e322f REF: sign-verify screen to ts 2025-12-06 18:49:32 +00:00
Ojok Emmanuel Nsubuga
01092cd130 REF: replace buffer with Uint8Array 2025-12-05 12:02:27 +00:00
overtorment
18deedd31c REF: remove remains of analytics 2025-11-27 22:20:01 +00:00
transifex-integration[bot]
6ae048db0e
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-26 08:58:21 +00:00
transifex-integration[bot]
bd3adefd38
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-26 08:58:03 +00:00
overtorment
754a67b388 OPS: retries of integration tests on ci 2025-11-25 20:58:12 +00:00
overtorment
cabc11f419 FIX: better display on Discovery screen 2025-11-25 20:58:12 +00:00
GLaDOS
f5d2373cd9
Merge pull request #8190 from BlueWallet/translations_loc-en-json--master_es_419
Updates for file loc/en.json in es_419
2025-11-25 18:42:02 +00:00
GLaDOS
8d3a9505a2
Merge pull request #8188 from BlueWallet/translations_loc-en-json--master_zh_CN
Updates for file loc/en.json in zh_CN
2025-11-25 17:46:38 +00:00
GLaDOS
2c72d7b284
Merge pull request #8189 from BlueWallet/translations_loc-en-json--master_de_DE
Updates for file loc/en.json in de_DE
2025-11-25 17:46:34 +00:00
Marcos Rodriguez Vélez
41539e4a63
Update project.pbxproj 2025-11-25 07:11:10 -05:00
GLaDOS
042e0cde85
Merge pull request #8185 from BlueWallet/refactor-ark-lightning
Refactor ark lightning
2025-11-25 10:11:02 +00:00
overtorment
fe9e975dbe OPS: simplify tests on ci and get rid of circleci 2025-11-25 08:38:41 +00:00
transifex-integration[bot]
f7afe79d49
Translate loc/en.json in de_DE
100% reviewed source file: 'loc/en.json'
on 'de_DE'.
2025-11-25 08:36:21 +00:00
transifex-integration[bot]
5501b46d6f
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2025-11-25 08:36:20 +00:00
transifex-integration[bot]
e6e49856ee
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-25 08:36:18 +00:00
GLaDOS
fc7d5e658b
Merge pull request #8183 from BlueWallet/fix-scanqr-error-breaks-flow
FIX: error in scanQR decoding breaks the whole scanning flow (closes …
2025-11-25 08:35:55 +00:00
GLaDOS
fb80a428e6
Merge pull request #8186 from BlueWallet/translations_loc-en-json--master_zh_CN
Updates for file loc/en.json in zh_CN
2025-11-25 03:53:32 +00:00
transifex-integration[bot]
6f964b0525
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-24 17:23:13 +00:00
transifex-integration[bot]
af55328140
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-24 17:22:46 +00:00
transifex-integration[bot]
79e8b66ffb
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-24 17:21:04 +00:00
transifex-integration[bot]
d7df1f7dfa
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-24 17:17:17 +00:00
transifex-integration[bot]
6cf729f23c
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-24 17:15:37 +00:00
transifex-integration[bot]
2c49d2747b
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-24 17:13:04 +00:00
transifex-integration[bot]
c50e069d4d
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-24 17:10:30 +00:00
transifex-integration[bot]
eeb7b7cd5a
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-24 17:10:17 +00:00
transifex-integration[bot]
08239ba3d1
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-24 17:08:26 +00:00
transifex-integration[bot]
e1c6c96b3c
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-24 17:08:16 +00:00
transifex-integration[bot]
ec513c28e9
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-24 17:08:06 +00:00
transifex-integration[bot]
5ba4cb2431
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-24 17:07:56 +00:00
transifex-integration[bot]
ddc44735b4
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-24 17:07:46 +00:00
transifex-integration[bot]
580b505c71
Translate loc/en.json in zh_CN
100% reviewed source file: 'loc/en.json'
on 'zh_CN'.
2025-11-24 17:06:18 +00:00
overtorment
f0edc102f9 REF: speed up wallet htlc claims 2025-11-24 16:45:45 +00:00
overtorment
ee7d808267 REF: speed up ark wallet 2025-11-24 16:22:06 +00:00
overtorment
18920d6efe FIX: error in scanQR decoding breaks the whole scanning flow (closes #8180) 2025-11-24 14:11:20 +00:00
Marcos Rodriguez
848a514580 OPS: Version bump 2025-11-23 16:22:06 -05:00
582 changed files with 65595 additions and 20649 deletions

View File

@ -1,85 +0,0 @@
version: 2.1
jobs:
lint:
docker:
- image: cimg/node:22.14.0
working_directory: ~/repo
steps:
- checkout
- restore_cache:
key: node_modules-{{ checksum "package-lock.json" }}
- run: test -d node_modules || npm ci
- save_cache:
key: node_modules-{{ checksum "package-lock.json" }}
paths:
- node_modules
# run tests!
- run:
command: npm run tslint && npm run lint
unit:
docker:
- image: cimg/node:22.14.0
working_directory: ~/repo
steps:
- checkout
- restore_cache:
key: node_modules-{{ checksum "package-lock.json" }}
- run: test -d node_modules || npm ci
- save_cache:
key: node_modules-{{ checksum "package-lock.json" }}
paths:
- node_modules
# run tests!
- run:
command: npm run unit
integration:
docker:
- image: cimg/node:22.14.0
environment:
RETRY: "1"
working_directory: ~/repo
resource_class: large
steps:
- checkout
- restore_cache:
key: node_modules-{{ checksum "package-lock.json" }}
- run: test -d node_modules || npm ci
- save_cache:
key: node_modules-{{ checksum "package-lock.json" }}
paths:
- node_modules
# run tests!
- run:
command: npm run jest || npm run jest || npm run jest || npm run jest
# Orchestrate our job run sequence
workflows:
build_and_test:
jobs:
- lint
- unit
- integration

View File

@ -7,10 +7,15 @@
}
},
"apps": {
"ios": {
"ios.debug": {
"type": "ios.app",
"binaryPath": "SPECIFY_PATH_TO_YOUR_APP_BINARY",
"build": "xcodebuild clean build -workspace ios/BlueWallet.xcworkspace -scheme BlueWallet -configuration Release -derivedDataPath ios/build -sdk iphonesimulator13.2"
"binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/BlueWallet.app",
"build": "xcodebuild -workspace ios/BlueWallet.xcworkspace -scheme BlueWallet -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build"
},
"ios.release": {
"type": "ios.app",
"binaryPath": "ios/build/Build/Products/Release-iphonesimulator/BlueWallet.app",
"build": "npx react-native codegen && xcodebuild -workspace ios/BlueWallet.xcworkspace -scheme BlueWallet -configuration Release -sdk iphonesimulator -derivedDataPath ios/build"
},
"android.debug": {
"type": "android.apk",
@ -28,7 +33,7 @@
"simulator": {
"type": "ios.simulator",
"device": {
"type": "iPhone 11"
"type": "iPhone 17"
}
},
"emulator": {
@ -39,9 +44,13 @@
}
},
"configurations": {
"ios": {
"ios.debug": {
"device": "simulator",
"app": "ios"
"app": "ios.debug"
},
"ios.release": {
"device": "simulator",
"app": "ios.release"
},
"android.debug": {
"device": "emulator",

View File

@ -21,6 +21,10 @@
"react-native/no-unused-styles": "error",
"react/no-is-mounted": "off",
"react-native/no-single-element-style-arrays": "error",
"react-hooks/refs": "off",
"react-hooks/immutability": "off",
"react-hooks/purity": "off",
"react-hooks/set-state-in-effect": "off",
"prettier/prettier": [
"warn",
{

View File

@ -16,7 +16,7 @@ Please provide:
* your phone model and OS version
* BlueWallet app version (settings->about->scroll down)
* self-test passes? Open settings->about->scroll down, tap "Run self-test"
* unique ID for our crash reporting service (settings->about->scroll down, tap "copy")
* unique ID for our crash reporting service (option 1: settings->about->scroll down, tap "copy")(option 2: open the settings app->apps->BlueWallet,double tap the unique id text field and select copy)
## Proposing a feature?

View File

@ -10,9 +10,13 @@ on:
- master
workflow_dispatch:
concurrency:
group: build-ios-release-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: macos-latest
runs-on: macos-26
timeout-minutes: 180
outputs:
new_build_number: ${{ steps.generate_build_number.outputs.build_number }}
@ -26,7 +30,7 @@ jobs:
steps:
- name: Checkout Project
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0 # Ensures the full Git history is
@ -65,13 +69,12 @@ jobs:
echo "Branch Name: ${{ env.CURRENT_BRANCH }}"
- name: Specify Node.js Version
uses: actions/setup-node@v4
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
node-version: 24
cache: 'npm'
cache-dependency-path: package-lock.json
- uses: maxim-lobanov/setup-xcode@v1
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: latest
@ -205,9 +208,9 @@ jobs:
echo -e "\033[1;34m======================================================\033[0m"
- name: Set Up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
with:
ruby-version: 3.1.6
ruby-version: 3.4.9
- name: System Debug Information
run: |
@ -244,7 +247,21 @@ jobs:
- name: Install Node Modules
run: npm ci --omit=dev --yes
- name: Cache CocoaPods
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
ios/Pods
~/Library/Caches/CocoaPods
~/.cocoapods/repos
key: ${{ runner.os }}-pods-ios-release-${{ hashFiles('ios/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-ios-release-
- name: Install CocoaPods Dependencies
env:
RCT_USE_RN_DEP: "1"
RCT_USE_PREBUILT_RNCORE: "1"
run: |
bundle exec fastlane ios install_pods
echo "CocoaPods dependencies installed successfully"
@ -430,7 +447,7 @@ jobs:
- name: Upload Build Logs
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build_logs
path: ./ios/build_logs/
@ -451,7 +468,7 @@ jobs:
- name: Upload IPA as Artifact
if: success()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: BlueWallet_IPA
path: ${{ env.IPA_OUTPUT_PATH }}
@ -463,7 +480,7 @@ jobs:
testflight-upload:
needs: build
runs-on: macos-latest
runs-on: macos-26
if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'testflight')
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
@ -473,20 +490,20 @@ jobs:
BRANCH_NAME: ${{ needs.build.outputs.branch_name }}
steps:
- name: Checkout Project
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set Up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
with:
ruby-version: 3.1.6
ruby-version: 3.4.9
- name: Install Dependencies with Bundler
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3 --quiet
- name: Download IPA from Artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: BlueWallet_IPA
path: ./
@ -530,7 +547,7 @@ jobs:
- name: Post PR Comment
if: success() && github.event_name == 'pull_request'
uses: actions/github-script@v6
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }}
PROJECT_VERSION: ${{ needs.build.outputs.project_version }}

187
.github/workflows/build-mac-catalyst.yml vendored Normal file
View File

@ -0,0 +1,187 @@
name: Build Mac Catalyst
on:
workflow_dispatch:
pull_request:
branches:
- master
types: [labeled, synchronize]
concurrency:
group: catalyst-build-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
build:
if: >
github.event_name == 'workflow_dispatch' ||
(github.event.action == 'labeled' && (github.event.label.name == 'mac-dmg' || github.event.label.name == 'testflight')) ||
github.event.action == 'synchronize'
runs-on: macos-15
timeout-minutes: 120
steps:
- name: Check PR labels
if: github.event_name == 'pull_request'
id: labels
env:
GH_TOKEN: ${{ github.token }}
run: |
LABELS=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" --jq '.[].name' | tr '\n' ',')
echo "all=${LABELS}" >> $GITHUB_OUTPUT
if [[ "$LABELS" == *"mac-dmg"* ]]; then
echo "has_mac_dmg=true" >> $GITHUB_OUTPUT
else
echo "has_mac_dmg=false" >> $GITHUB_OUTPUT
fi
if [[ "$LABELS" == *"testflight"* ]] && [[ "$LABELS" == *"mac-dmg"* ]]; then
echo "upload_testflight=true" >> $GITHUB_OUTPUT
else
echo "upload_testflight=false" >> $GITHUB_OUTPUT
fi
echo "Labels on PR: ${LABELS}"
- name: Skip if mac-dmg label not present
if: github.event_name == 'pull_request' && steps.labels.outputs.has_mac_dmg != 'true'
run: |
echo "mac-dmg label not found on PR — skipping build."
exit 0
- name: Checkout project
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- name: Setup Node.js
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: 'npm'
- name: Setup Xcode
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: latest
- name: Set up Ruby
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
with:
ruby-version: 3.4.9
bundler-cache: true
- name: Install Node modules
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
run: npm ci
- name: Cache CocoaPods
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
ios/Pods
~/Library/Caches/CocoaPods
~/.cocoapods/repos
key: ${{ runner.os }}-pods-catalyst-${{ hashFiles('ios/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-catalyst-
- name: Install CocoaPods dependencies
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
env:
SKIP_APP_STORE_CONNECT_AUTH: '1'
RCT_USE_RN_DEP: "1"
RCT_USE_PREBUILT_RNCORE: "1"
run: bundle exec fastlane ios install_pods
- name: Create temporary keychain for signing
if: (github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true') && steps.labels.outputs.upload_testflight == 'true'
run: |
security create-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
- name: Build Mac Catalyst app with Fastlane
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
id: build_catalyst
run: bundle exec fastlane ios build_catalyst_app_lane
env:
SKIP_APP_STORE_CONNECT_AUTH: '1'
SKIP_CLEAR_DERIVED_DATA: '1'
CATALYST_SIGNING_IDENTITY: ${{ steps.labels.outputs.upload_testflight == 'true' && secrets.CATALYST_SIGNING_IDENTITY || '' }}
CATALYST_TEAM_ID: ${{ steps.labels.outputs.upload_testflight == 'true' && secrets.CATALYST_TEAM_ID || '' }}
GIT_URL: ${{ steps.labels.outputs.upload_testflight == 'true' && secrets.GIT_URL || '' }}
GIT_ACCESS_TOKEN: ${{ steps.labels.outputs.upload_testflight == 'true' && secrets.GIT_ACCESS_TOKEN || '' }}
MATCH_READONLY: ${{ steps.labels.outputs.upload_testflight == 'true' && 'false' || 'true' }}
KEYCHAIN_NAME: ${{ steps.labels.outputs.upload_testflight == 'true' && 'build' || '' }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
- name: Upload Mac Catalyst DMG
id: upload_dmg
if: success() && (github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true')
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: BlueWallet-Mac-Catalyst
path: ${{ steps.build_catalyst.outputs.catalyst_dmg_path }}
if-no-files-found: warn
- name: Create App Store Connect API Key JSON
if: success() && steps.labels.outputs.upload_testflight == 'true'
run: echo '${{ secrets.APPLE_API_KEY_CONTENT }}' > ./appstore_api_key.json
- name: Upload to TestFlight
if: success() && steps.labels.outputs.upload_testflight == 'true'
run: bundle exec fastlane ios upload_catalyst_to_testflight
env:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
CATALYST_TEAM_ID: ${{ secrets.CATALYST_TEAM_ID }}
TEAM_ID: ${{ secrets.TEAM_ID }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
LATEST_COMMIT_MESSAGE: ${{ github.event.pull_request.title || 'Manual build' }}
- name: Cleanup App Store Connect API Key JSON
if: always() && steps.labels.outputs.upload_testflight == 'true'
run: rm -f ./appstore_api_key.json
- name: Cleanup temporary keychain
if: always() && steps.labels.outputs.upload_testflight == 'true'
run: security delete-keychain build.keychain || true
- name: Comment on PR with DMG link
if: success() && github.event_name == 'pull_request' && steps.labels.outputs.has_mac_dmg == 'true'
env:
GH_TOKEN: ${{ github.token }}
UPLOADED_TO_TF: ${{ steps.labels.outputs.upload_testflight }}
run: |
ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/${{ steps.upload_dmg.outputs.artifact-id }}"
COMMENT_TAG="<!-- catalyst-dmg-link -->"
TF_LINE=""
if [[ "$UPLOADED_TO_TF" == "true" ]]; then
TF_LINE=$'\n\n**Also uploaded to TestFlight.** Check [App Store Connect](https://appstoreconnect.apple.com) for the build.'
fi
COMMENT_FILE="$(mktemp)"
{
printf '%s\n' "${COMMENT_TAG}"
printf '### Mac Catalyst Build\n\n'
printf 'The Mac Catalyst DMG is ready for download:\n\n'
printf '[Download BlueWallet-Mac-Catalyst.dmg](%s)\n' "${ARTIFACT_URL}"
if [[ -n "$TF_LINE" ]]; then
printf '%s\n' "${TF_LINE}"
fi
printf '<sub>Built from `%s`"
} >"${COMMENT_FILE}"
gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
--paginate --jq '.[] | select(.body | contains("<!-- catalyst-dmg-link -->")) | .id' | \
while read -r comment_id; do
gh api -X DELETE "repos/${{ github.repository }}/issues/comments/${comment_id}" || true
done
gh pr comment "${{ github.event.pull_request.number }}" --body-file "${COMMENT_FILE}"

View File

@ -11,75 +11,110 @@ on:
jobs:
buildReleaseApk:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: Checkout project
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: "0"
- name: Free disk space (Android build)
shell: bash
run: |
df -h
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /opt/ghc || true
sudo rm -rf /usr/local/share/boost || true
sudo rm -rf /usr/local/lib/android/sdk/ndk || true
docker system prune -af || true
sudo rm -rf /usr/local/lib/android/sdk/system-images || true
sudo rm -rf /usr/local/lib/android/sdk/emulator || true
rm -rf ~/.gradle/caches/modules-2/files-2.1 || true
rm -rf ~/.gradle/caches/build-cache || true
rm -rf ~/.npm/_cacache ~/.cache || true
sudo rm -rf /home/runner/work/_temp || true
df -h
- name: Specify node version
uses: actions/setup-node@v4
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
node-version: 24
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Use specific Java version for sdkmanager to work
uses: actions/setup-java@v4
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: Install node_modules
run: npm ci --omit=dev --yes
- name: Use gradle caches
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle', 'android/**/*.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Set up Android SDK
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
- name: Install Android SDK components
run: |
yes | sdkmanager --licenses
sdkmanager "platforms;android-36" "platform-tools" "build-tools;36.0.0" "ndk;27.1.12297006"
- name: Install node_modules (include dev deps for patch-package)
run: npm ci --yes
- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
with:
ruby-version: 3.1.6
ruby-version: 3.4.9
bundler-cache: true
- name: Cache Ruby Gems
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-
- name: Generate Build Number based on timestamp
id: build_number
run: |
NEW_BUILD_NUMBER="$(date +%s)"
echo "NEW_BUILD_NUMBER=$NEW_BUILD_NUMBER" >> $GITHUB_ENV
echo "build_number=$NEW_BUILD_NUMBER" >> $GITHUB_OUTPUT
- name: Prepare Keystore
run: bundle exec fastlane android prepare_keystore
env:
KEYSTORE_FILE_HEX: ${{ secrets.KEYSTORE_FILE_HEX }}
- name: Update Version Code, Build, and Sign APK
- name: Build and sign APK
id: build_and_sign_apk
run: |
bundle exec fastlane android update_version_build_and_sign_apk
run: bundle exec fastlane android build_release_apk
env:
BUILD_NUMBER: ${{ env.NEW_BUILD_NUMBER }}
BUILD_NUMBER: ${{ steps.build_number.outputs.build_number }}
KEYSTORE_FILE_HEX: ${{ secrets.KEYSTORE_FILE_HEX }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
- name: Upload build logs on failure
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: android-build-logs
path: |
fastlane/logs/**/*.log
android/**/*.log
android/**/build/**/*.log
android/**/outputs/logs/**/*.log
android/**/reports/**/*.log
if-no-files-found: warn
- name: Determine APK Filename and Path
id: determine_apk_path
run: |
BUILD_NUMBER=${{ steps.build_number.outputs.build_number }}
VERSION_NAME=$(grep versionName android/app/build.gradle | awk '{print $2}' | tr -d '"')
BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}
BRANCH_NAME=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9_-]/_/g')
if [ -n "$BRANCH_NAME" ] && [ "$BRANCH_NAME" != "master" ]; then
EXPECTED_FILENAME="BlueWallet-${VERSION_NAME}-${NEW_BUILD_NUMBER}-${BRANCH_NAME}.apk"
EXPECTED_FILENAME="BlueWallet-${VERSION_NAME}-${BUILD_NUMBER}-${BRANCH_NAME}.apk"
else
EXPECTED_FILENAME="BlueWallet-${VERSION_NAME}-${NEW_BUILD_NUMBER}.apk"
EXPECTED_FILENAME="BlueWallet-${VERSION_NAME}-${BUILD_NUMBER}.apk"
fi
APK_PATH="android/app/build/outputs/apk/release/${EXPECTED_FILENAME}"
@ -87,32 +122,32 @@ jobs:
echo "APK_PATH=${APK_PATH}" >> $GITHUB_ENV
- name: Upload APK as artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: signed-apk
path: ${{ env.APK_PATH }}
if-no-files-found: error
browserstack:
runs-on: ubuntu-latest
runs-on: macos-26
needs: buildReleaseApk
if: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'browserstack') }}
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
with:
ruby-version: 3.1.6
ruby-version: 3.4.9
bundler-cache: true
- name: Install dependencies with Bundler
run: bundle install --jobs 4 --retry 3
- name: Download APK artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: signed-apk
@ -127,4 +162,4 @@ jobs:
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bundle exec fastlane upload_to_browserstack_and_comment
run: bundle exec fastlane upload_to_browserstack_and_comment

View File

@ -4,35 +4,87 @@ name: Tests
# https://medium.com/@reime005/the-best-ci-cd-for-react-native-with-e2e-support-4860b4aaab29
env:
TRAVIS: 1
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
HD_MNEMONIC_BIP84: ${{ secrets.HD_MNEMONIC_BIP84 }}
on: [pull_request]
jobs:
test:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout project
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- name: Specify node version
uses: actions/setup-node@v4
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
node-version: 24
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install node_modules
run: npm ci
run: npm ci || npm ci
- name: Run tests
run: npm test || npm test || npm test || npm test
run: npm run lint
unit:
runs-on: ubuntu-latest
steps:
- name: Checkout project
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- name: Specify node version
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: 'npm'
- name: Install node_modules
run: npm ci || npm ci
- name: Run tests
run: npm run unit
env:
BIP47_HD_MNEMONIC: ${{ secrets.BIP47_HD_MNEMONIC}}
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
HD_MNEMONIC_BIP49: ${{ secrets.HD_MNEMONIC_BIP49 }}
HD_MNEMONIC_OLD: ${{ secrets.HD_MNEMONIC_OLD }}
HD_MNEMONIC_BIP49_MANY_TX: ${{ secrets.HD_MNEMONIC_BIP49_MANY_TX }}
HD_MNEMONIC_BIP84: ${{ secrets.HD_MNEMONIC_BIP84 }}
HD_MNEMONIC_BREAD: ${{ secrets.HD_MNEMONIC_BREAD }}
FAULTY_ZPUB: ${{ secrets.FAULTY_ZPUB }}
MNEMONICS_COBO: ${{ secrets.MNEMONICS_COBO }}
MNEMONICS_COLDCARD: ${{ secrets.MNEMONICS_COLDCARD }}
integration:
runs-on: ubuntu-latest
steps:
- name: Checkout project
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- name: Specify node version
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: 'npm'
- name: Install node_modules
run: npm ci || npm ci
- name: Run tests
run: npm run integration || npm run integration || npm run integration || npm run integration
env:
BIP47_HD_MNEMONIC: ${{ secrets.BIP47_HD_MNEMONIC}}
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
HD_MNEMONIC_BIP49: ${{ secrets.HD_MNEMONIC_BIP49 }}
HD_MNEMONIC_OLD: ${{ secrets.HD_MNEMONIC_OLD }}
HD_MNEMONIC_BIP49_MANY_TX: ${{ secrets.HD_MNEMONIC_BIP49_MANY_TX }}
HD_MNEMONIC_BIP84: ${{ secrets.HD_MNEMONIC_BIP84 }}
HD_MNEMONIC_BREAD: ${{ secrets.HD_MNEMONIC_BREAD }}
@ -40,93 +92,3 @@ jobs:
MNEMONICS_COBO: ${{ secrets.MNEMONICS_COBO }}
MNEMONICS_COLDCARD: ${{ secrets.MNEMONICS_COLDCARD }}
RETRY: 1
e2e:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: true
android: false
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: npm and gradle caches in /mnt
run: |
rm -rf ~/.npm
rm -rf ~/.gradle
sudo mkdir -p /mnt/.npm
sudo mkdir -p /mnt/.gradle
sudo chown -R runner /mnt/.npm
sudo chown -R runner /mnt/.gradle
ln -s /mnt/.npm /home/runner/
ln -s /mnt/.gradle /home/runner/
- name: Create artifacts directory on /mnt
run: |
sudo mkdir -p /mnt/artifacts
sudo chown -R runner /mnt/artifacts
- name: Specify node version
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Use gradle caches
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Install node_modules
run: npm ci --omit=dev --yes
- name: Use specific Java version for sdkmanager to work
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Build
run: npm run e2e:release-build
- name: Install dev deps needed for tests
run: npm i
- name: Run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 31
avd-name: Pixel_API_29_AOSP
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none -partition-size 2047
arch: x86_64
script: npm run e2e:release-test -- --record-videos all --record-logs all --take-screenshots all --headless -d 200000 -R 5 --artifacts-location /mnt/artifacts
- uses: actions/upload-artifact@v4
if: failure()
with:
name: e2e-test-videos
path: /mnt/artifacts/

152
.github/workflows/e2e-android.yml vendored Normal file
View File

@ -0,0 +1,152 @@
name: Tests e2e Android
on: [pull_request]
permissions:
contents: read
concurrency:
group: e2e-android-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Free disk space (Ubuntu)
run: |
echo "Disk before cleanup:" && df -h
sudo rm -rf /usr/share/dotnet /opt/ghc
sudo apt-get clean
sudo rm -rf /opt/ghc || true
sudo rm -rf /usr/local/share/boost || true
sudo rm -rf /usr/local/lib/android/sdk/ndk || true
sudo docker system prune -af || true
sudo rm -rf /usr/local/lib/android/sdk/system-images || true
sudo rm -rf /usr/local/lib/android/sdk/emulator || true
rm -rf ~/.gradle/caches/modules-2/files-2.1 || true
rm -rf ~/.gradle/caches/build-cache || true
rm -rf ~/.npm/_cacache ~/.cache || true
sudo rm -rf /home/runner/work/_temp || true
echo "Disk after cleanup:" && df -h
- name: Specify node version
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: 'npm'
- name: Use gradle caches
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle', 'android/**/*.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Install node_modules
run: npm ci || npm ci
- name: Use specific Java version for sdkmanager to work
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin'
java-version: '17'
- name: Build
run: npm run e2e:release-build || npm run e2e:release-build
- name: Package APKs
run: |
tar -czf bluewallet-android-apks.tar.gz \
android/app/build/outputs/apk/release/app-release.apk \
android/app/build/outputs/apk/androidTest/release/app-release-androidTest.apk
- name: Upload APKs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: bluewallet-android-apks
path: bluewallet-android-apks.tar.gz
retention-days: 3
compression-level: 0
if-no-files-found: error
test:
runs-on: ubuntu-24.04
needs: build
env:
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
HD_MNEMONIC_BIP84: ${{ secrets.HD_MNEMONIC_BIP84 }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Free disk space (Ubuntu)
run: |
echo "Disk before cleanup:" && df -h
sudo rm -rf /usr/share/dotnet /opt/ghc
sudo apt-get clean
sudo rm -rf /opt/ghc || true
sudo rm -rf /usr/local/share/boost || true
sudo rm -rf /usr/local/lib/android/sdk/ndk || true
sudo docker system prune -af || true
rm -rf ~/.npm/_cacache ~/.cache || true
sudo rm -rf /home/runner/work/_temp || true
echo "Disk after cleanup:" && df -h
- name: Ensure artifacts directory
run: mkdir -p ${{ github.workspace }}/artifacts
- name: Specify node version
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: 'npm'
- name: Install node_modules
run: npm ci || npm ci
- name: Use specific Java version for sdkmanager to work
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin'
java-version: '17'
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Download APKs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bluewallet-android-apks
- name: Restore APKs
run: tar -xzf bluewallet-android-apks.tar.gz
- name: Run tests
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
with:
api-level: 36
profile: pixel
avd-name: Pixel_API_29_AOSP
force-avd-creation: true
enable-hw-keyboard: true
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none -partition-size 2047
arch: x86_64
script: npm run e2e:release-test -- --record-videos failing --record-logs failing --take-screenshots failing --headless --retries 4 --reuse --artifacts-location ${{ github.workspace }}/artifacts
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: e2e-android-videos
path: ${{ github.workspace }}/artifacts

233
.github/workflows/e2e-ios.yml vendored Normal file
View File

@ -0,0 +1,233 @@
name: Tests e2e iOS
on: [pull_request]
permissions:
contents: read
concurrency:
group: e2e-ios-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: macos-26
env:
BUILD_CONFIGURATION: Release
CCACHE_MAXSIZE: "2G"
CCACHE_DIR: /Users/runner/.ccache
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: 'npm'
- name: Setup Ruby
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
with:
ruby-version: "3.4.9"
bundler-cache: true
- name: Install Node dependencies
run: npm ci || npm ci
- name: Cache CocoaPods
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
ios/Pods
~/Library/Caches/CocoaPods
~/.cocoapods/repos
key: ${{ runner.os }}-pods-prebuilt-${{ hashFiles('ios/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-prebuilt-
- name: Install ccache
run: brew install ccache
- name: Cache ccache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.ccache
key: ${{ runner.os }}-ccache-${{ github.sha }}
restore-keys: |
${{ runner.os }}-ccache-
- name: Delete extension and watch targets
run: |
bundle exec ruby <<'RUBY'
require 'xcodeproj'
project_path = 'ios/BlueWallet.xcodeproj'
project = Xcodeproj::Project.open(project_path)
target_names = %w[BlueWalletWatch WidgetsExtension Stickers]
embed_phase_names = ['Embed Watch Content', 'Embed Foundation Extensions']
removed_any = false
target_names.each do |target_name|
target = project.targets.find { |t| t.name == target_name }
next unless target
puts "Removing target #{target_name}"
target.dependencies.each(&:remove_from_project)
target.build_phases.each(&:remove_from_project)
project.targets.delete(target)
removed_any = true
end
main_target = project.targets.find { |t| t.name == 'BlueWallet' }
if main_target
main_target.build_phases.select { |phase| embed_phase_names.include?(phase.display_name) }.each do |phase|
puts "Removing build phase #{phase.display_name}"
phase.remove_from_project
main_target.build_phases.delete(phase)
removed_any = true
end
end
if removed_any
project.save
puts 'Extension and watch target references removed'
else
puts 'No extension or watch targets found'
end
RUBY
- name: Remove extension and watch schemes
run: |
rm -f ios/BlueWallet.xcodeproj/xcshareddata/xcschemes/BlueWalletWatch.xcscheme
rm -f ios/BlueWallet.xcodeproj/xcshareddata/xcschemes/WidgetsExtension.xcscheme
rm -f ios/BlueWallet.xcodeproj/xcshareddata/xcschemes/Stickers.xcscheme
- name: Install CocoaPods dependencies
env:
RCT_USE_RN_DEP: "1"
RCT_USE_PREBUILT_RNCORE: "1"
USE_CCACHE: "1"
run: bundle exec fastlane ios install_pods || bundle exec fastlane ios install_pods
- name: Reset ccache stats
run: ccache -z || true
- name: Build iOS simulator app
working-directory: ios
env:
RCT_NO_LAUNCH_PACKAGER: "1"
CCACHE_BINARY: /opt/homebrew/bin/ccache
run: |
set -eo pipefail
build() {
xcodebuild \
-workspace BlueWallet.xcworkspace \
-scheme BlueWallet \
-configuration "${BUILD_CONFIGURATION}" \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath build \
CLANG_ENABLE_EXPLICIT_MODULES=NO \
SWIFT_ENABLE_EXPLICIT_MODULES=NO \
build
}
build || build
- name: ccache stats
if: always()
run: ccache -s || true
- name: Package simulator app
run: |
APP_DIR="ios/build/Build/Products/${BUILD_CONFIGURATION}-iphonesimulator/BlueWallet.app"
if [ ! -d "$APP_DIR" ]; then
echo "Simulator app not found at $APP_DIR"
find ios/build -maxdepth 5 -name '*.app' || true
exit 1
fi
tar -czf BlueWallet.app.tar.gz -C "$(dirname "$APP_DIR")" "$(basename "$APP_DIR")"
- name: Upload simulator app
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: bluewallet-ios-app
path: BlueWallet.app.tar.gz
retention-days: 3
compression-level: 0
if-no-files-found: error
test:
runs-on: macos-26
needs: build
env:
HD_MNEMONIC: ${{ secrets.HD_MNEMONIC }}
HD_MNEMONIC_BIP84: ${{ secrets.HD_MNEMONIC_BIP84 }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: 'npm'
- name: Install Node dependencies
run: npm ci || npm ci
- name: Install applesimutils
run: |
brew tap wix/brew
brew install applesimutils
- name: Download simulator app
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bluewallet-ios-app
- name: Restore simulator app
run: |
mkdir -p ios/build/Build/Products/Release-iphonesimulator
tar -xzf BlueWallet.app.tar.gz -C ios/build/Build/Products/Release-iphonesimulator
# Pre-boot simulator so first detox launchApp lands warm.
- name: Pre-boot iOS simulator
run: |
DEVICE_TYPE=$(jq -r '.devices.simulator.device.type' .detoxrc.json)
UDID=$(applesimutils --list --byType "$DEVICE_TYPE" | jq -r '.[0].udid // empty')
if [ -z "$UDID" ]; then
echo "ERROR: no simulator of type '$DEVICE_TYPE' found"
exit 1
fi
xcrun simctl boot "$UDID" 2>/dev/null || true
xcrun simctl bootstatus "$UDID" -b
xcrun simctl launch "$UDID" com.apple.springboard >/dev/null 2>&1 || true
# Cut animations so detox sync stays steady on slow CI VMs; Reduce Motion makes reanimated skip to final value.
- name: Disable simulator animations
run: |
defaults write com.apple.iphonesimulator SlowMotionAnimation -bool NO
xcrun simctl spawn booted defaults write com.apple.Accessibility ReduceMotionEnabled -bool true
xcrun simctl spawn booted notifyutil -p com.apple.Accessibility.ReduceMotionStatusDidChange
- name: Run detox tests
timeout-minutes: 360
run: |
npm run e2e:test:ios-release -- \
--record-videos failing \
--record-logs failing \
--take-screenshots failing \
--headless \
--retries 3 \
--reuse \
--artifacts-location ./artifacts
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: e2e-ios-videos
path: ./artifacts/

View File

@ -1,99 +0,0 @@
name: Lock Files Update
on:
workflow_dispatch:
push:
branches:
- master
jobs:
pod-update:
runs-on: macos-15
permissions:
contents: write
steps:
- name: Checkout master branch
uses: actions/checkout@v4
with:
ref: master # Ensures we're checking out the master branch
fetch-depth: 0 # Ensures full history to enable branch deletion and recreation
- name: Delete existing branch
run: |
git push origin --delete pod-update-branch || echo "Branch does not exist, continuing..."
git branch -D pod-update-branch || echo "Local branch does not exist, continuing..."
- name: Create new branch from master
run: git checkout -b pod-update-branch # Create a new branch from the master branch
- name: Specify node version
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install node modules
run: npm ci
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.6
bundler-cache: true
- name: Install and update Ruby Gems
run: |
bundle install
- name: Install CocoaPods Dependencies
run: |
cd ios
pod install
pod update
- name: Check for changes
id: check-changes
run: |
git diff --quiet package-lock.json ios/Podfile.lock || echo "Changes detected"
continue-on-error: true
- name: Stop job if no changes
if: steps.check-changes.outcome == 'success'
run: |
echo "No changes detected in package-lock.json or Podfile.lock. Stopping the job."
exit 0
- name: Commit changes
if: steps.check-changes.outcome != 'success'
run: |
git add package-lock.json ios/Podfile.lock
git commit -m "Update lock files"
# Step 10: Get the list of changed files for PR description
- name: Get changed files for PR description
id: get-changes
if: steps.check-changes.outcome != 'success'
run: |
git diff --name-only HEAD^ HEAD > changed_files.txt
echo "CHANGES=$(cat changed_files.txt)" >> $GITHUB_ENV
# Step 11: Push the changes and create the PR using the LockFiles PAT
- name: Push and create PR
if: steps.check-changes.outcome != 'success'
run: |
git push origin pod-update-branch
gh pr create --title "Lock Files Updates" --body "The following lock files were updated:\n\n${{ env.CHANGES }}" --base master
env:
GITHUB_TOKEN: ${{ secrets.LOCKFILES_WORKFLOW }} # Use the LockFiles PAT for PR creation
cleanup:
runs-on: macos-15
if: github.event.pull_request.merged == true || github.event.pull_request.state == 'closed'
needs: pod-update
steps:
- name: Delete branch after PR merge/close
run: |
git push origin --delete pod-update-branch

5
.gitignore vendored
View File

@ -82,14 +82,19 @@ artifacts/
# Editors
.vscode/
/.vs
.claude
*.mx
*.realm
*.realm.lock
android/app/.project
android/.settings/org.eclipse.buildship.core.prefs
android/app/.classpath
android/.settings/org.eclipse.buildship.core.prefs
android/.project
android/app/.settings/org.eclipse.jdt.core.prefs
android/.settings/org.eclipse.buildship.core.prefs
android/app/.classpath
android/app/.project
fastlane/README.md
fastlane/report.xml

View File

@ -1 +1 @@
3.1.6
3.4.9

View File

@ -1,4 +1,4 @@
import { NavigationContainer } from '@react-navigation/native';
import { NavigationContainer, NavigationContainerRef, ParamListBase } from '@react-navigation/native';
import React from 'react';
import { useColorScheme } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
@ -13,7 +13,7 @@ import { StorageProvider } from './components/Context/StorageProvider';
const App = () => {
const colorScheme = useColorScheme();
useLogger(navigationRef);
useLogger(navigationRef as unknown as React.RefObject<NavigationContainerRef<ParamListBase>>);
return (
<SizeClassProvider>

View File

@ -1,139 +0,0 @@
/* eslint react/prop-types: "off", react-native/no-inline-styles: "off" */
import React, { forwardRef } from 'react';
import { Dimensions, Platform, Pressable, StyleSheet, TextInput, View } from 'react-native';
import { Icon, Text } from '@rneui/themed';
import { useTheme } from './components/themes';
import { useLocale } from '@react-navigation/native';
const { height, width } = Dimensions.get('window');
const aspectRatio = height / width;
let isIpad;
if (aspectRatio > 1.6) {
isIpad = false;
} else {
isIpad = true;
}
/**
* TODO: remove this comment once this file gets properly converted to typescript.
*
* @type {React.FC<any>}
*/
export const BlueButtonLink = forwardRef((props, ref) => {
const { colors } = useTheme();
return (
<Pressable accessibilityRole="button" style={({ pressed }) => [styles.blueButtonLink, pressed && styles.pressed]} {...props} ref={ref}>
<Text style={{ color: colors.foregroundColor, textAlign: 'center', fontSize: 16 }}>{props.title}</Text>
</Pressable>
);
});
export const BlueCard = props => {
return <View {...props} style={{ padding: 20 }} />;
};
export const BlueText = ({ bold = false, ...props }) => {
const { colors } = useTheme();
const { direction } = useLocale();
const style = StyleSheet.compose(
{
color: colors.foregroundColor,
writingDirection: direction,
fontWeight: bold ? 'bold' : 'normal',
},
props.style,
);
return <Text {...props} style={style} />;
};
export const BlueTextCentered = props => {
const { colors } = useTheme();
return <Text {...props} style={{ color: colors.foregroundColor, textAlign: 'center' }} />;
};
export const BlueFormLabel = props => {
const { colors } = useTheme();
const { direction } = useLocale();
return (
<Text
{...props}
style={{
color: colors.foregroundColor,
fontWeight: '400',
marginHorizontal: 20,
writingDirection: direction,
}}
/>
);
};
export const BlueFormMultiInput = props => {
const { colors } = useTheme();
return (
<TextInput
multiline
underlineColorAndroid="transparent"
numberOfLines={4}
editable={!props.editable}
style={{
paddingHorizontal: 8,
paddingVertical: 16,
flex: 1,
marginTop: 5,
marginHorizontal: 20,
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
borderWidth: 1,
borderBottomWidth: 0.5,
borderRadius: 4,
backgroundColor: colors.inputBackgroundColor,
color: colors.foregroundColor,
textAlignVertical: 'top',
}}
autoCorrect={false}
autoCapitalize="none"
spellCheck={false}
{...props}
selectTextOnFocus={false}
keyboardType={Platform.OS === 'android' ? 'visible-password' : 'default'}
/>
);
};
export class is {
static ipad() {
return isIpad;
}
}
export function BlueBigCheckmark({ style = {} }) {
const defaultStyles = {
backgroundColor: '#ccddf9',
width: 120,
height: 120,
borderRadius: 60,
alignSelf: 'center',
justifyContent: 'center',
marginTop: 0,
marginBottom: 0,
};
const mergedStyles = { ...defaultStyles, ...style };
return (
<View style={mergedStyles}>
<Icon name="check" size={50} type="font-awesome" color="#0f5cc0" />
</View>
);
}
const styles = StyleSheet.create({
blueButtonLink: {
minWidth: 100,
minHeight: 36,
justifyContent: 'center',
},
pressed: {
opacity: 0.6,
},
});

76
CLAUDE.md Normal file
View File

@ -0,0 +1,76 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
BlueWallet is a Bitcoin & Lightning Network wallet built with React Native and Electrum. Cross-platform mobile app (iOS/Android/macOS via Catalyst).
## Common Commands
```bash
# Development
npm start # Start Metro bundler
npm run ios # Run on iOS
npm run android # Run on Android
# Testing
npm test # Full suite (lint + unit + integration)
npm run lint # ESLint + TypeScript check + unused loc keys
npm run lint:fix # Auto-fix linting issues
npm run unit # Jest unit tests only
# E2E Testing (Detox)
npm run e2e:debug # Debug build and test on Android
npm run e2e:release-test # Release build test
# Clean builds
npm run clean # Full clean (gradle, cache, node_modules)
npm run clean:ios # iOS clean (Pods + node_modules)
npm run android:clean # Android clean
```
## Architecture
**Directory Structure:**
- `components/` - React components and Context providers (SettingsProvider, StorageProvider)
- `class/` - Core business logic including wallet implementations in `class/wallets/`
- `blue_modules/` - Utility modules (BlueElectrum, currency, encryption, etc.)
- `screen/` - Navigation screens organized by feature (wallets, send, receive, settings, lnd)
- `navigation/` - React Navigation setup with typed param lists
- `hooks/` - Custom React hooks (useStorage, useSettings, useBiometrics, etc.)
- `loc/` - Localization files (en.json as source, 55+ languages)
- `models/` - Type definitions for units, fiat, block explorers
- `tests/unit/`, `tests/integration/`, `tests/e2e/` - Test suites
**Wallet System:**
Multiple wallet implementations in `class/wallets/`: Legacy, SegWit (P2SH, Bech32), Taproot, HD variants, Lightning (Custodian, Ark), Multisig, Watch-only. Types defined in `class/wallets/types.ts`.
**State Management:**
React Context providers wrap the app. Custom hooks expose state logic. Realm for database, AsyncStorage for persistence, Keychain for secrets.
**Navigation:**
React Navigation 7.x with native stack. Typed params in `navigation/DetailViewStackParamList.ts` and other param list files.
## Code Conventions
**Commit Prefixes:** REL, FIX, ADD, REF, TST, OPS, DOC (e.g., `"ADD: new feature"`)
**TypeScript:** All new files must be TypeScript. Strict mode enabled.
**Dependencies:** Do not add new dependencies without strong justification. Bonus for removing dependencies.
**Patches:** Local fixes to `node_modules` live in `patches/` and are applied by `patch-package` on `postinstall`. Each patch is documented in `patches/README.md` (what/why + upstream issue link); update it when adding or removing a patch.
**Components:** New components go in `components/`, not legacy `BlueComponents.js`.
**Linting Rules:**
- No inline styles in React Native (`react-native/no-inline-styles`: error)
- No unused styles (`react-native/no-unused-styles`: error)
- Prettier: single quotes, 140 char width, trailing commas
**Localization:** Keys in `loc/en.json`. Run `find-unused-loc.js` to detect unused keys. See `loc/vocabulary.md` for the canonical glossary of Bitcoin/Lightning terms and their per-language renderings — use it as ground truth when translating or generating translations with LLMs.
## Testing
Unit tests in `tests/unit/` use Jest with `assert`. Test setup mocks React Native modules (Clipboard, Push Notifications, Keychain, etc.). Integration tests require environment variables for test mnemonics (HD_MNEMONIC, HD_MNEMONIC_BIP84, etc.).

View File

@ -25,3 +25,9 @@ Do *not* add new dependencies. Bonus points if you manage to actually remove a d
All new files must be in typescript. Bonus points if you convert some of the existing files to typescript.
New components must go in `components/`. Bonus points if you refactor some of old components in `BlueComponents.js` to separate files.
Don't forget to add tests. Bonus points for e2e tests.
# PRs
When submitting PR, it must include screenshot (from the emulator or the device) how the proposed change looks, even better - a video; and a short description of why (it was implemented) and how (it works under the hood).

12
Gemfile
View File

@ -1,13 +1,19 @@
source "https://rubygems.org"
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby "3.1.6"
gem "fastlane", "~> 2.228.0"
ruby "3.4.9"
gem "fastlane", "~> 2.234.0"
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
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'
gem 'logger'
gem 'benchmark'
gem 'mutex_m'
# Required for App Store Connect API
gem "jwt"

View File

@ -1,11 +1,9 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
activesupport (7.2.2.1)
CFPropertyList (3.0.8)
abbrev (0.1.2)
activesupport (7.2.3.1)
base64
benchmark (>= 0.3)
bigdecimal
@ -14,19 +12,19 @@ GEM
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
minitest (>= 5.1, < 6)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1144.0)
aws-sdk-core (3.229.0)
aws-partitions (1.1252.0)
aws-sdk-core (3.247.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@ -34,19 +32,19 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.110.0)
aws-sdk-core (~> 3, >= 3.228.0)
aws-sdk-kms (1.127.0)
aws-sdk-core (~> 3, >= 3.247.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.196.1)
aws-sdk-core (~> 3, >= 3.228.0)
aws-sdk-s3 (1.223.0)
aws-sdk-core (~> 3, >= 3.247.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
benchmark (0.4.1)
bigdecimal (3.2.2)
benchmark (0.5.0)
bigdecimal (4.1.2)
claide (1.1.0)
cocoapods (1.15.2)
addressable (~> 2.8)
@ -89,8 +87,9 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.3.3)
connection_pool (2.5.3)
concurrent-ruby (1.3.7)
connection_pool (3.0.2)
csv (3.3.5)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
@ -99,10 +98,11 @@ GEM
drb (2.2.3)
emoji_regex (3.2.3)
escape (0.0.4)
ethon (0.16.0)
ethon (0.18.0)
ffi (>= 1.15.0)
logger
excon (0.112.0)
faraday (1.10.4)
faraday (1.10.5)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@ -114,32 +114,36 @@ GEM
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.1)
faraday-multipart (1.2.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday-retry (1.0.4)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.228.0)
CFPropertyList (>= 2.3, < 4.0.0)
fastimage (2.4.1)
fastlane (2.234.0)
CFPropertyList (>= 2.3, < 5.0.0)
abbrev (~> 0.1)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
aws-sdk-s3 (~> 1.197)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
base64 (~> 0.2)
benchmark (>= 0.1.0)
bundler (>= 1.17.3, < 5.0.0)
colored (~> 1.2)
commander (~> 4.6)
csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
@ -147,20 +151,24 @@ GEM
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
fastlane-sirp (>= 1.1.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-env (>= 1.6.0, <= 2.1.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
logger (>= 1.6, < 2.0)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
mutex_m (~> 0.3)
naturally (~> 2.2)
nkf (~> 0.2)
optparse (>= 0.1.1, < 1.0.0)
ostruct (>= 0.1.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
@ -175,53 +183,55 @@ GEM
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-browserstack (0.3.4)
rest-client (~> 2.0, >= 2.0.2)
fastlane-plugin-bugsnag (2.3.1)
fastlane-plugin-bugsnag (3.0.0)
abbrev
git
xml-simple
fastlane-plugin-bugsnag_sourcemaps_upload (0.2.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
ffi (1.17.2)
fastlane-sirp (1.1.0)
ffi (1.17.3)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
git (3.1.1)
git (4.3.1)
activesupport (>= 5.0)
addressable (~> 2.8)
process_executer (~> 1.3)
process_executer (~> 4.0)
rchardet (~> 1.9)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
google-apis-androidpublisher_v3 (0.100.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (0.18.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
googleauth (~> 1.9)
httpclient (>= 2.8.3, < 3.a)
mini_mime (~> 1.0)
mutex_m
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-iamcredentials_v1 (0.27.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-playcustomapp_v1 (0.17.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.62.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.5.0)
google-cloud-storage (1.47.0)
google-cloud-env (2.1.1)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.6.0)
google-cloud-storage (1.60.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-apis-core (>= 0.18, < 2)
google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (>= 0.42)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
googleauth (~> 1.9)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
googleauth (1.11.2)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
@ -232,22 +242,22 @@ GEM
domain_name (~> 0.5)
httpclient (2.9.0)
mutex_m
i18n (1.14.7)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
json (2.13.2)
json (2.19.5)
jwt (2.10.2)
base64
logger (1.7.0)
mime-types (3.7.0)
logger
mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2025.0805)
mime-types-data (3.2026.0317)
mini_magick (4.13.2)
mini_mime (1.1.5)
minitest (5.25.5)
minitest (5.27.0)
molinillo (0.8.0)
multi_json (1.17.0)
multi_json (1.21.1)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.3.0)
@ -255,13 +265,15 @@ GEM
naturally (2.3.0)
netrc (0.11.0)
nkf (0.2.0)
optparse (0.6.0)
optparse (0.8.1)
os (1.1.4)
ostruct (0.6.3)
plist (3.7.2)
process_executer (1.3.0)
process_executer (4.0.2)
track_open_instances (~> 0.1)
public_suffix (4.0.7)
rake (13.3.0)
rchardet (1.9.0)
rake (13.4.2)
rchardet (1.10.0)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
@ -271,33 +283,33 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (3.1.2)
rexml (3.4.1)
retriable (3.4.1)
rexml (3.4.4)
rouge (3.28.0)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
securerandom (0.4.1)
security (0.1.5)
signet (0.20.0)
signet (0.21.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
jwt (>= 1.5, < 4.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
track_open_instances (0.1.15)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
typhoeus (1.4.1)
ethon (>= 0.9.0)
typhoeus (1.6.0)
ethon (>= 0.18.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uber (0.1.0)
@ -322,17 +334,157 @@ PLATFORMS
DEPENDENCIES
activesupport (>= 6.1.7.5, != 7.1.0)
benchmark
bigdecimal
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
concurrent-ruby (< 1.3.4)
fastlane (~> 2.228.0)
concurrent-ruby (< 1.3.8)
fastlane (~> 2.234.0)
fastlane-plugin-browserstack
fastlane-plugin-bugsnag
fastlane-plugin-bugsnag_sourcemaps_upload
jwt
logger
mutex_m
xcodeproj (< 1.26.0)
CHECKSUMS
CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261
abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242
activesupport (7.2.3.1) sha256=11ebed516a43a0bb47346227a35ebae4d9427465a7c9eb197a03d5c8d283cb34
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
algoliasearch (1.27.5) sha256=26c1cddf3c2ec4bd60c148389e42702c98fdac862881dc6b07a4c0b89ffec853
artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263
atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f
aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b
aws-partitions (1.1252.0) sha256=b44c74136ebd634d35f3fb8fd37def5214db21b9375f22c6954dbe7a7f2a449d
aws-sdk-core (3.247.0) sha256=789864594ce8cef05ee3d81fa8ed506099280bda6ea12a7612b8b7c5e5e62851
aws-sdk-kms (1.127.0) sha256=5d540b6afb9574327202989db2217741211e1cce3fb443ad0e1e37de730202e5
aws-sdk-s3 (1.223.0) sha256=655e382af34926caa76b77cf0171caed5f61ff52b8b58ae50f6f3e22c39e6cbc
aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00
babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e
cocoapods (1.15.2) sha256=f0f5153de8d028d133b96f423e04f37fb97a1da0d11dda581a9f46c0cba4090a
cocoapods-core (1.15.2) sha256=322650d97fe1ad4c0831a09669764b888bd91c6d79d0f6bb07281a17667a2136
cocoapods-deintegrate (1.0.5) sha256=517c2a448ef563afe99b6e7668704c27f5de9e02715a88ee9de6974dc1b3f6a2
cocoapods-downloader (2.1) sha256=bb6ebe1b3966dc4055de54f7a28b773485ac724fdf575d9bee2212d235e7b6d1
cocoapods-plugins (1.0.0) sha256=725d17ce90b52f862e73476623fd91441b4430b742d8a071000831efb440ca9a
cocoapods-search (1.0.1) sha256=1b133b0e6719ed439bd840e84a1828cca46425ab73a11eff5e096c3b2df05589
cocoapods-trunk (1.6.0) sha256=5f5bda8c172afead48fa2d43a718cf534b1313c367ba1194cebdeb9bfee9ed31
cocoapods-try (1.2.0) sha256=145b946c6e7747ed0301d975165157951153d27469e6b2763c83e25c84b9defe
colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c
colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a
commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9
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
digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07
domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933
dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b
escape (0.0.4) sha256=e49f44ae2b4f47c6a3abd544ae77fe4157802794e32f19b8e773cbc4dcec4169
ethon (0.18.0) sha256=b598afc9f30448cb068b850714b7d6948e941476095d04f90a4ac65b8d6efcb2
excon (0.112.0) sha256=daf9ac3a4c2fc9aa48383a33da77ecb44fa395111e973084d5c52f6f214ae0f0
faraday (1.10.5) sha256=b144f1d2b045652fa820b5f532723e1643cc28b93dae911d784e5c5f88e8f6ed
faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb
faraday-em_http (1.0.0) sha256=7a3d4c7079789121054f57e08cd4ef7e40ad1549b63101f38c7093a9d6c59689
faraday-em_synchrony (1.0.1) sha256=bf3ce45dcf543088d319ab051f80985ea6d294930635b7a0b966563179f81750
faraday-excon (1.1.0) sha256=b055c842376734d7f74350fe8611542ae2000c5387348d9ba9708109d6e40940
faraday-httpclient (1.0.1) sha256=4c8ff1f0973ff835be8d043ef16aaf54f47f25b7578f6d916deee8399a04d33b
faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757
faraday-net_http (1.0.2) sha256=63992efea42c925a20818cf3c0830947948541fdcf345842755510d266e4c682
faraday-net_http_persistent (1.2.0) sha256=0b0cbc8f03dab943c3e1cc58d8b7beb142d9df068b39c718cd83e39260348335
faraday-patron (1.0.0) sha256=dc2cd7b340bb3cc8e36bcb9e6e7eff43d134b6d526d5f3429c7a7680ddd38fa7
faraday-rack (1.0.0) sha256=ef60ec969a2bb95b8dbf24400155aee64a00fc8ba6c6a4d3968562bcc92328c0
faraday-retry (1.0.4) sha256=dc659233777fabf96c69c2ffe56c0a5d2c102af90321a42cc6c90157bcd716aa
faraday_middleware (1.2.1) sha256=d45b78c8ee864c4783fbc276f845243d4a7918a67301c052647bacabec0529e9
fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20
fastlane (2.234.0) sha256=b74835681ad9a8e9c0931a5727dad1bab433895ac534c864a1ed5749625d26e9
fastlane-plugin-browserstack (0.3.4) sha256=a4f3e4a552e2390a4733570857512571535912100ffada177d5374413f2c1333
fastlane-plugin-bugsnag (3.0.0) sha256=8ddac4b79cb4b5d00432cccd5789a9e1a1119c29f7773a27d01b1d8a2363915d
fastlane-plugin-bugsnag_sourcemaps_upload (0.2.0) sha256=a05afaefa81a7bf56c36386dddeb0931db31ead6886e3eae24f9683bda1a064d
fastlane-sirp (1.1.0) sha256=10bc94f9682efd8e1badfb31452a76dd8981f1f3a33717c765fde6d75b54d847
ffi (1.17.3) sha256=0e9f39f7bb3934f77ad6feab49662be77e87eedcdeb2a3f5c0234c2938563d4c
fourflusher (2.3.1) sha256=1b3de61c7c791b6a4e64f31e3719eb25203d151746bb519a0292bff1065ccaa9
fuzzy_match (2.0.4) sha256=b5de4f95816589c5b5c3ad13770c0af539b75131c158135b3f3bbba75d0cfca5
gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939
git (4.3.1) sha256=91ca566c39766a033e61a148c8f470908bd4786b818f8f3ff566d3a9a0200c50
google-apis-androidpublisher_v3 (0.100.0) sha256=7a82935bee985190e8fe23bf5e53df3a27d65dd084114bb71b846b617de16489
google-apis-core (0.18.0) sha256=96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee
google-apis-iamcredentials_v1 (0.27.0) sha256=9289f29968610754ef11d98b9ec627f0153f3e2616fef839aef096de529f6d1e
google-apis-playcustomapp_v1 (0.17.0) sha256=d5bc90b705f3f862bab4998086449b0abe704ee1685a84821daa90ca7fa95a78
google-apis-storage_v1 (0.62.0) sha256=f62467c36df53287fb0252ebb4da85f9e25d7b4c5809d045c2aab1fc307760c1
google-cloud-core (1.8.0) sha256=e572edcbf189cfcab16590628a516cec3f4f63454b730e59f0b36575120281cf
google-cloud-env (2.1.1) sha256=cf4bb8c7d517ee1ea692baedf06e0b56ce68007549d8d5a66481aa9f97f46999
google-cloud-errors (1.6.0) sha256=1da8476dd706ad04b9d32e3c4b90d07d3463b37d6407cb56d41342ea7647d0a1
google-cloud-storage (1.60.0) sha256=b21b752d37945d678a4533be5ef4303f15d33a964d8bc709c7c41c3600f650db
googleauth (1.11.2) sha256=7e6bacaeed7aea3dd66dcea985266839816af6633e9f5983c3c2e0e40a44731e
highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479
http-accept (1.7.0) sha256=c626860682bfbb3b46462f8c39cd470fd7b0584f61b3cc9df5b2e9eb9972a126
http-cookie (1.0.8) sha256=b14fe0445cf24bf9ae098633e9b8d42e4c07c3c1f700672b09fbfe32ffd41aa6
httpclient (2.9.0) sha256=4b645958e494b2f86c2f8a2f304c959baa273a310e77a2931ddb986d83e498c8
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59
jwt (2.10.2) sha256=31e1ee46f7359883d5e622446969fe9c118c3da87a0b1dca765ce269c3a0c4f4
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56
mime-types-data (3.2026.0317) sha256=77f078a4d8631d52b842ba77099734b06eddb7ad339d792e746d2272b67e511b
mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
molinillo (0.8.0) sha256=efbff2716324e2a30bccd3eba1ff3a735f4d5d53ffddbc6a2f32c0ca9433045d
multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080
multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8
mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751
nanaimo (0.3.0) sha256=aaaedc60497070b864a7e220f7c4b4cad3a0daddda2c30055ba8dae306342376
nap (1.1.0) sha256=949691660f9d041d75be611bb2a8d2fd559c467537deac241f4097d9b5eea576
naturally (2.3.0) sha256=459923cf76c2e6613048301742363200c3c7e4904c324097d54a67401e179e01
netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
nkf (0.2.0) sha256=fbc151bda025451f627fafdfcb3f4f13d0b22ae11f58c6d3a2939c76c5f5f126
optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a
os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42
process_executer (4.0.2) sha256=c73eb646d450044241c973a8360f6326e33ec5ad933f7acf503f6f3579873a71
public_suffix (4.0.7) sha256=8be161e2421f8d45b0098c042c06486789731ea93dc3a896d30554ee38b573b8
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
rchardet (1.10.0) sha256=d5ea2ed61a720a220f1914778208e718a0c7ed2a484b6d357ba695aa7001390f
representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace
rest-client (2.1.0) sha256=35a6400bdb14fae28596618e312776c158f7ebbb0ccad752ff4fa142bf2747e3
retriable (3.4.1) sha256=fb3f114b7d492121c158c01f3d5152b5a615c5b70d5877d0bc08c7ec3725c3bc
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20
ruby-macho (2.5.1) sha256=9075e52e0f9270b552a90b24fcc6219ad149b0d15eae1bc364ecd0ac8984f5c9
ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7
signet (0.21.0) sha256=d617e9fbf24928280d39dcfefba9a0372d1c38187ffffd0a9283957a10a8cd5b
simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b
terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea
terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91
track_open_instances (0.1.15) sha256=7f0e48821e6b4c881daaa40fb1583e308937c22a9c84883c150b399c3b5c3029
trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3
tty-cursor (0.7.1) sha256=79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48
tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50
tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542
typhoeus (1.6.0) sha256=bacc41c23e379547e29801dc235cd1699b70b955a1ba3d32b2b877aa844c331d
tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc
unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a
word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7
xcodeproj (1.25.1) sha256=9a2310dccf6d717076e86f602b17c640046b6f1dfe64480044596f6f2f13dc84
xcpretty (0.4.1) sha256=b14c50e721f6589ee3d6f5353e2c2cfcd8541fa1ea16d6c602807dd7327f3892
xcpretty-travis-formatter (1.0.1) sha256=aacc332f17cb7b2cba222994e2adc74223db88724fe76341483ad3098e232f93
xml-simple (1.1.9) sha256=d21131e519c86f1a5bc2b6d2d57d46e6998e47f18ed249b25cad86433dbd695d
RUBY VERSION
ruby 3.1.6p260
ruby 3.4.9
BUNDLED WITH
2.3.27
4.0.7

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 BlueWallet developers
Copyright (c) 2026 BlueWallet developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,7 +1,6 @@
# BlueWallet - A Bitcoin & Lightning Wallet
[![GitHub tag](https://img.shields.io/badge/dynamic/json.svg?url=https://raw.githubusercontent.com/BlueWallet/BlueWallet/master/package.json&query=$.version&label=Version)](https://github.com/BlueWallet/BlueWallet)
[![CircleCI](https://circleci.com/gh/BlueWallet/BlueWallet.svg?style=svg)](https://circleci.com/gh/BlueWallet/BlueWallet)
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)
![](https://img.shields.io/github/license/BlueWallet/BlueWallet.svg)
@ -105,7 +104,7 @@ Grab an issue from [the backlog](https://github.com/BlueWallet/BlueWallet/issues
## Translations
We accept translations via [Transifex](https://www.transifex.com/bluewallet/bluewallet/)
We accept translations via [Transifex](https://explore.transifex.com/bluewallet/bluewallet/)
To participate you need to:
1. Sign up to Transifex
@ -117,6 +116,10 @@ Please note the values in curly braces should not be translated. These are the n
Transifex automatically creates Pull Request when language reaches 100% translation. We also trigger this by hand before each release, so don't worry if you can't translate everything, every word counts.
### Vocabulary glossaries
[`loc/vocabulary.md`](loc/vocabulary.md) + the per-language files under [`loc/vocabulary/`](loc/vocabulary/) are the canonical glossary of Bitcoin/Lightning terms (Wallet, Vault, Seed, Mnemonic, Passphrase, Multisig, Payment Code, Coin Control, …) and their chosen rendering in each locale, with the reasoning behind each choice and ⚠️ anti-meaning callouts (e.g. Passcode ≠ Password, Change-output ≠ verb "to change"). Use them as ground truth when translating by hand or when feeding `loc/<lang>.json` to an LLM — terminology consistency across screens is the difference between "looks translated" and "is correct for a Bitcoin wallet". When you change a shipped string, update the matching row in the same PR.
## Q&A
Builds automated and tested with BrowserStack

View File

@ -1,2 +0,0 @@
connection.project.dir=
eclipse.preferences.version=1

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

View File

@ -1,2 +1,2 @@
connection.project.dir=..
eclipse.preferences.version=1
eclipse.preferences.version=1

View File

@ -0,0 +1,4 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.source=17

View File

@ -19,9 +19,9 @@ react {
/* Variants */
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// skip the bundling of the JS bundle and the assets. Default is "debug", "debugOptimized".
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "prodDebug"]
// debuggableVariants = ["liteDebug", "liteDebugOptimized", "prodDebug", "prodDebugOptimized"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
@ -87,12 +87,14 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "7.2.2"
versionName "8.0.1"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
// Keep compatibility across react-native-capture-protection flavor changes.
missingDimensionStrategy "react-native-capture-protection", "callbackTiramisu", "base"
}
lintOptions {
lint {
abortOnError false
checkReleaseBuilds false
}
@ -100,13 +102,12 @@ android {
sourceSets {
main {
assets.srcDirs = ['src/main/assets', 'src/main/res/assets']
java.srcDirs = ['src/main/java', '../../blue_modules/Views/SegmentedControl/android']
}
}
buildTypes {
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
@ -122,14 +123,21 @@ task copyFiatUnits(type: Copy) {
preBuild.dependsOn(copyFiatUnits)
// Ensure fiat units are available before codegen scans JS sources
tasks.configureEach { task ->
if (task.name == 'generateCodegenSchemaFromJavaScript') {
task.dependsOn(copyFiatUnits)
}
}
dependencies {
androidTestImplementation('com.wix:detox:+')
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.work:work-runtime-ktx:2.10.5'
implementation 'androidx.core:core-ktx:1.18.0'
implementation 'androidx.work:work-runtime-ktx:2.11.2'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.compose.ui:ui:1.9.4'
implementation 'androidx.compose.ui:ui:1.10.6'
implementation 'androidx.compose.material3:material3:1.3.2'
implementation 'androidx.preference:preference-ktx:1.2.1'
@ -138,9 +146,7 @@ dependencies {
} else {
implementation jscFlavor
}
androidTestImplementation('com.wix:detox:0.1.1')
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
}
apply plugin: 'com.google.gms.google-services' // Google Services plugin

View File

@ -13,7 +13,6 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />
@ -34,27 +33,18 @@
android:networkSecurityConfig="@xml/network_security_config"
android:configChanges="uiMode">
<meta-data
android:name="com.dieam.reactnativepushnotification.notification_channel_name"
android:value="BlueWallet notifications" />
<meta-data
android:name="com.dieam.reactnativepushnotification.notification_channel_description"
android:value="Notifications about incoming payments" />
<meta-data
android:name="com.dieam.reactnativepushnotification.notification_foreground"
android:value="true" />
<meta-data
android:name="com.dieam.reactnativepushnotification.channel_create_default"
android:value="true" />
<meta-data
android:name="com.dieam.reactnativepushnotification.notification_color"
android:resource="@color/white" />
<meta-data
android:name="firebase_messaging_auto_init_enabled"
android:value="false" />
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="false" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/notification_icon" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/foreground_color" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
@ -64,16 +54,6 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
<receiver
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".BitcoinPriceWidget" android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@ -130,19 +110,11 @@
</intent-filter>
</activity-alias>
<service
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleTask"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:windowSoftInputMode="adjustResize"
android:exported="true">

View File

@ -16,7 +16,7 @@
"AMD": {
"endPointKey": "AMD",
"locale": "hy-AM",
"source": "CoinDesk",
"source": "Coinbase",
"symbol": "֏",
"country": "Armenia (Armenian Dram)"
},
@ -44,7 +44,7 @@
"AWG": {
"endPointKey": "AWG",
"locale": "nl-AW",
"source": "CoinDesk",
"source": "Coinbase",
"symbol": "ƒ",
"country": "Aruba (Aruban Florin)"
},
@ -142,7 +142,7 @@
"HRK": {
"endPointKey": "HRK",
"locale": "hr-HR",
"source": "CoinDesk",
"source": "Coinbase",
"symbol": "HRK",
"country": "Croatia (Croatian Kuna)"
},
@ -191,7 +191,7 @@
"ISK": {
"endPointKey": "ISK",
"locale": "is-IS",
"source": "CoinDesk",
"source": "Coinbase",
"symbol": "kr",
"country": "Iceland (Icelandic Króna)"
},
@ -254,7 +254,7 @@
"MZN": {
"endPointKey": "MZN",
"locale": "seh-MZ",
"source": "CoinDesk",
"source": "Coinbase",
"symbol": "MTn",
"country": "Mozambique (Mozambican Metical)"
},
@ -282,7 +282,7 @@
"OMR": {
"endPointKey": "OMR",
"locale": "ar-OM",
"source": "CoinDesk",
"source": "Coinbase",
"symbol": "ر.ع.",
"country": "Oman (Omani Rial)"
},
@ -303,14 +303,14 @@
"PYG": {
"endPointKey": "PYG",
"locale": "es-PY",
"source": "CoinDesk",
"source": "Coinbase",
"symbol": "₲",
"country": "Paraguay (Paraguayan Guarani)"
},
"QAR": {
"endPointKey": "QAR",
"locale": "ar-QA",
"source": "CoinDesk",
"source": "Coinbase",
"symbol": "ر.ق.",
"country": "Qatar (Qatari Riyal)"
},
@ -324,7 +324,7 @@
"RSD": {
"endPointKey": "RSD",
"locale": "sr-RS",
"source": "CoinDesk",
"source": "Coinbase",
"symbol": "DIN",
"country": "Serbia (Serbian Dinar)"
},
@ -380,7 +380,7 @@
"TZS": {
"endPointKey": "TZS",
"locale": "en-TZ",
"source": "CoinDesk",
"source": "Coinbase",
"symbol": "TSh",
"country": "Tanzania (Tanzanian Shilling)"
},
@ -394,14 +394,14 @@
"UGX": {
"endPointKey": "UGX",
"locale": "en-UG",
"source": "CoinDesk",
"source": "Coinbase",
"symbol": "USh",
"country": "Uganda (Ugandan Shilling)"
},
"UYU": {
"endPointKey": "UYU",
"locale": "es-UY",
"source": "CoinDesk",
"source": "Coinbase",
"symbol": "$",
"country": "Uruguay (Uruguayan Peso)"
},
@ -422,7 +422,7 @@
"XAF": {
"endPointKey": "XAF",
"locale": "fr-CF",
"source": "CoinDesk",
"source": "Coinbase",
"symbol": "Fr",
"country": "Central African Republic (Central African Franc)"
},
@ -436,8 +436,8 @@
"GHS": {
"endPointKey": "GHS",
"locale": "en-GH",
"source": "CoinDesk",
"source": "Coinbase",
"symbol": "₵",
"country": "Ghana (Ghanaian Cedi)"
}
}
}

View File

@ -39,31 +39,43 @@ class BitcoinPriceWidget : AppWidgetProvider() {
// Try to load cached data first
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val cachedPrice = sharedPref.getString("previous_price", null)
val preferredCurrency = sharedPref.getString("preferredCurrency", "USD")
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", null)
if (cachedPrice != null) {
// Show cached data immediately
val preferredCurrency = sharedPref.getString("preferredCurrency", "USD")
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", "en-US")
try {
val localeParts = preferredCurrencyLocale?.split("-") ?: listOf("en", "US")
val locale = if (localeParts.size == 2) {
java.util.Locale(localeParts[0], localeParts[1])
} else {
java.util.Locale.getDefault()
}
val locale = preferredCurrencyLocale
?.let { runCatching { java.util.Locale.forLanguageTag(it) }.getOrNull() }
?.takeIf { it.language.isNotBlank() }
?: java.util.Locale.getDefault()
val currencyFormat = java.text.NumberFormat.getCurrencyInstance(locale)
val currency = java.util.Currency.getInstance(preferredCurrency ?: "USD")
currencyFormat.currency = currency
currencyFormat.maximumFractionDigits = 0
val parsedCached = cachedPrice.toDoubleOrNull()?.toInt()
views.setViewVisibility(R.id.loading_indicator, View.GONE)
views.setViewVisibility(R.id.price_value, View.VISIBLE)
views.setViewVisibility(R.id.last_updated_label, View.VISIBLE)
views.setViewVisibility(R.id.last_updated_time, View.VISIBLE)
views.setTextViewText(R.id.price_value, currencyFormat.format(cachedPrice.toDouble().toInt()))
views.setTextViewText(R.id.last_updated_time, java.text.SimpleDateFormat("hh:mm a", java.util.Locale.getDefault()).format(java.util.Date()))
views.setViewVisibility(R.id.price_arrow_container, View.GONE)
if (parsedCached != null) {
views.setViewVisibility(R.id.price_value, View.VISIBLE)
views.setViewVisibility(R.id.last_updated_label, View.VISIBLE)
views.setViewVisibility(R.id.last_updated_time, View.VISIBLE)
views.setTextViewText(R.id.price_value, currencyFormat.format(parsedCached))
views.setTextViewText(
R.id.last_updated_time,
java.text.SimpleDateFormat("hh:mm a", java.util.Locale.getDefault()).format(java.util.Date())
)
} else {
// If parsing fails, show loading state
views.setViewVisibility(R.id.price_value, View.GONE)
views.setViewVisibility(R.id.last_updated_label, View.GONE)
views.setViewVisibility(R.id.last_updated_time, View.GONE)
views.setViewVisibility(R.id.loading_indicator, View.VISIBLE)
}
} catch (e: Exception) {
Log.e(TAG, "Error displaying cached price", e)
// Show loading state if cache display fails

View File

@ -11,14 +11,13 @@ import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.react.modules.fresco.FrescoModule
import com.facebook.react.modules.i18nmanager.I18nUtil
import io.bluewallet.bluewallet.components.segmentedcontrol.CustomSegmentedControlPackage
import io.bluewallet.bluewallet.components.segmentedcontrol.SegmentedControlPackage
class MainApplication : Application(), ReactApplication {
@ -66,26 +65,25 @@ class MainApplication : Application(), ReactApplication {
}
}
override val reactNativeHost: ReactNativeHost =
override val reactNativeHost: ReactNativeHost by lazy {
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
override fun getPackages() =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(CustomSegmentedControlPackage())
add(SegmentedControlPackage())
add(SettingsPackage())
}
override fun getJSMainModuleName(): String = "index"
override fun getUseDeveloperSupport() = BuildConfig.DEBUG
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
override fun getJSMainModuleName() = "index"
}
}
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override val reactHost: ReactHost by lazy {
getDefaultReactHost(applicationContext, reactNativeHost)
}
override fun onCreate() {
super.onCreate()
@ -101,12 +99,15 @@ class MainApplication : Application(), ReactApplication {
val sharedI18nUtilInstance = I18nUtil.getInstance()
sharedI18nUtilInstance.allowRTL(applicationContext, true)
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
// Initialize Fresco before RN mounts views. FrescoModule init can lag behind the first
// frame (e.g. UnlockWith logo) when OkHttp/SSL warms up network security config.
if (!FrescoModule.hasBeenInitialized()) {
Fresco.initialize(this)
}
loadReactNative(this)
initializeDeviceUID()
initializeBugsnag()
}

View File

@ -4,12 +4,13 @@ import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import java.util.UUID
import com.facebook.react.module.annotations.ReactModule
import io.bluewallet.bluewallet.NativeSettingsModuleSpec
class SettingsModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
@ReactModule(name = SettingsModule.NAME)
class SettingsModule(reactContext: ReactApplicationContext) : NativeSettingsModuleSpec(reactContext) {
private val sharedPref: SharedPreferences = reactContext.getSharedPreferences(
"group.io.bluewallet.bluewallet",
@ -22,10 +23,7 @@ class SettingsModule(reactContext: ReactApplicationContext) : ReactContextBaseJa
private const val DEVICE_UID_COPY_KEY = "deviceUIDCopy"
private const val CLEAR_FILES_ON_LAUNCH_KEY = "clearFilesOnLaunch"
private const val DO_NOT_TRACK_KEY = "donottrack"
}
override fun getName(): String {
return "SettingsModule"
const val NAME = "SettingsModule"
}
/**
@ -33,7 +31,7 @@ class SettingsModule(reactContext: ReactApplicationContext) : ReactContextBaseJa
* Uses the same Android ID as react-native-device-info's getUniqueId()
*/
@ReactMethod
fun initializeDeviceUID(promise: Promise) {
override fun initializeDeviceUID(promise: Promise) {
try {
val isDoNotTrackEnabled = sharedPref.getString(DO_NOT_TRACK_KEY, "0") == "1"
@ -86,7 +84,7 @@ class SettingsModule(reactContext: ReactApplicationContext) : ReactContextBaseJa
* Get the device UID
*/
@ReactMethod
fun getDeviceUID(promise: Promise) {
override fun getDeviceUID(promise: Promise) {
try {
val isDoNotTrackEnabled = sharedPref.getString(DO_NOT_TRACK_KEY, "0") == "1"
@ -107,7 +105,7 @@ class SettingsModule(reactContext: ReactApplicationContext) : ReactContextBaseJa
* Get the device UID copy (for Settings display)
*/
@ReactMethod
fun getDeviceUIDCopy(promise: Promise) {
override fun getDeviceUIDCopy(promise: Promise) {
try {
val deviceUIDCopy = sharedPref.getString(DEVICE_UID_COPY_KEY, "")
promise.resolve(deviceUIDCopy)
@ -121,7 +119,7 @@ class SettingsModule(reactContext: ReactApplicationContext) : ReactContextBaseJa
* Set the clearFilesOnLaunch preference
*/
@ReactMethod
fun setClearFilesOnLaunch(value: Boolean, promise: Promise) {
override fun setClearFilesOnLaunch(value: Boolean, promise: Promise) {
try {
sharedPref.edit()
.putBoolean(CLEAR_FILES_ON_LAUNCH_KEY, value)
@ -138,7 +136,7 @@ class SettingsModule(reactContext: ReactApplicationContext) : ReactContextBaseJa
* Get the clearFilesOnLaunch preference
*/
@ReactMethod
fun getClearFilesOnLaunch(promise: Promise) {
override fun getClearFilesOnLaunch(promise: Promise) {
try {
val value = sharedPref.getBoolean(CLEAR_FILES_ON_LAUNCH_KEY, false)
promise.resolve(value)
@ -152,7 +150,7 @@ class SettingsModule(reactContext: ReactApplicationContext) : ReactContextBaseJa
* Set Do Not Track setting
*/
@ReactMethod
fun setDoNotTrack(enabled: Boolean, promise: Promise) {
override fun setDoNotTrack(enabled: Boolean, promise: Promise) {
try {
val value = if (enabled) "1" else "0"
sharedPref.edit()
@ -184,7 +182,7 @@ class SettingsModule(reactContext: ReactApplicationContext) : ReactContextBaseJa
* Get Do Not Track setting
*/
@ReactMethod
fun getDoNotTrack(promise: Promise) {
override fun getDoNotTrack(promise: Promise) {
try {
val value = sharedPref.getString(DO_NOT_TRACK_KEY, "0")
val enabled = value == "1"
@ -199,7 +197,7 @@ class SettingsModule(reactContext: ReactApplicationContext) : ReactContextBaseJa
* Open the settings activity from JavaScript
*/
@ReactMethod
fun openSettings(promise: Promise) {
override fun openSettings(promise: Promise) {
try {
val intent = android.content.Intent(reactApplicationContext, SettingsActivity::class.java)
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)

View File

@ -1,16 +1,29 @@
package io.bluewallet.bluewallet
import com.facebook.react.ReactPackage
import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.uimanager.ViewManager
class SettingsPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(SettingsModule(reactContext))
class SettingsPackage : TurboReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
return if (name == SettingsModule.NAME) SettingsModule(reactContext) else null
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
val moduleInfo = ReactModuleInfo(
SettingsModule.NAME,
SettingsModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
false, // hasConstants
false, // isCxxModule
true // isTurboModule
)
mapOf(SettingsModule.NAME to moduleInfo)
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()
}

View File

@ -205,16 +205,23 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Cor
preferredCurrency: String?,
preferredCurrencyLocale: String?
) {
val parsedPrevious = previousPrice?.toDoubleOrNull()
val currencyFormat = getCurrencyFormat(preferredCurrency, preferredCurrencyLocale)
views.apply {
setViewVisibility(R.id.loading_indicator, View.GONE)
setTextViewText(R.id.price_value, currencyFormat.format(previousPrice?.toDouble()?.toInt()))
setTextViewText(R.id.last_updated_time, currentTime)
setViewVisibility(R.id.price_value, View.VISIBLE)
setViewVisibility(R.id.last_updated_label, View.VISIBLE)
setViewVisibility(R.id.last_updated_time, View.VISIBLE)
setViewVisibility(R.id.price_arrow_container, View.GONE)
if (parsedPrevious != null) {
setTextViewText(R.id.price_value, currencyFormat.format(parsedPrevious.toInt()))
setViewVisibility(R.id.price_value, View.VISIBLE)
setViewVisibility(R.id.last_updated_label, View.VISIBLE)
setViewVisibility(R.id.last_updated_time, View.VISIBLE)
} else {
setViewVisibility(R.id.price_value, View.GONE)
setViewVisibility(R.id.last_updated_label, View.GONE)
setViewVisibility(R.id.last_updated_time, View.GONE)
}
setTextViewText(R.id.last_updated_time, currentTime)
}
}
@ -226,37 +233,45 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Cor
preferredCurrency: String?,
preferredCurrencyLocale: String?
) {
val currentPrice = fetchedPrice.toDouble().toInt()
val currentPrice = fetchedPrice.toDoubleOrNull()?.toInt()
val currencyFormat = getCurrencyFormat(preferredCurrency, preferredCurrencyLocale)
views.apply {
setViewVisibility(R.id.loading_indicator, View.GONE)
setTextViewText(R.id.price_value, currencyFormat.format(currentPrice))
setTextViewText(R.id.last_updated_time, currentTime)
setViewVisibility(R.id.price_value, View.VISIBLE)
setViewVisibility(R.id.last_updated_label, View.VISIBLE)
setViewVisibility(R.id.last_updated_time, View.VISIBLE)
if (currentPrice != null) {
setTextViewText(R.id.price_value, currencyFormat.format(currentPrice))
setTextViewText(R.id.last_updated_time, currentTime)
setViewVisibility(R.id.price_value, View.VISIBLE)
setViewVisibility(R.id.last_updated_label, View.VISIBLE)
setViewVisibility(R.id.last_updated_time, View.VISIBLE)
if (previousPrice != null) {
setViewVisibility(R.id.price_arrow_container, View.VISIBLE)
setTextViewText(R.id.previous_price, currencyFormat.format(previousPrice.toDouble().toInt()))
setImageViewResource(
R.id.price_arrow,
if (currentPrice > previousPrice.toDouble().toInt()) android.R.drawable.arrow_up_float else android.R.drawable.arrow_down_float
)
val previousParsed = previousPrice?.toDoubleOrNull()?.toInt()
if (previousParsed != null) {
setViewVisibility(R.id.price_arrow_container, View.VISIBLE)
setTextViewText(R.id.previous_price, currencyFormat.format(previousParsed))
setImageViewResource(
R.id.price_arrow,
if (currentPrice > previousParsed) android.R.drawable.arrow_up_float else android.R.drawable.arrow_down_float
)
} else {
setViewVisibility(R.id.price_arrow_container, View.GONE)
}
} else {
// Fallback to loading state if parsing failed
setViewVisibility(R.id.price_value, View.GONE)
setViewVisibility(R.id.last_updated_label, View.GONE)
setViewVisibility(R.id.last_updated_time, View.GONE)
setViewVisibility(R.id.price_arrow_container, View.GONE)
setViewVisibility(R.id.loading_indicator, View.VISIBLE)
}
}
}
private fun getCurrencyFormat(currencyCode: String?, localeString: String?): NumberFormat {
val localeParts = localeString?.split("-") ?: listOf("en", "US")
val locale = if (localeParts.size == 2) {
Locale(localeParts[0], localeParts[1])
} else {
Locale.getDefault()
}
val locale = localeString
?.let { runCatching { Locale.forLanguageTag(it) }.getOrNull() }
?.takeIf { it.language.isNotBlank() }
?: Locale.getDefault()
val currencyFormat = NumberFormat.getCurrencyInstance(locale)
val currency = try {
Currency.getInstance(currencyCode ?: "USD")

View File

@ -1,47 +0,0 @@
package io.bluewallet.bluewallet.components.segmentedcontrol
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
class CustomSegmentedControlManager : SimpleViewManager<CustomSegmentedControl>() {
companion object {
const val REACT_CLASS = "CustomSegmentedControl"
private const val ON_CHANGE_EVENT = "onChangeEvent"
}
override fun getName(): String = REACT_CLASS
override fun createViewInstance(reactContext: ThemedReactContext): CustomSegmentedControl {
return CustomSegmentedControl(reactContext)
}
@ReactProp(name = "values")
fun setValues(view: CustomSegmentedControl, values: ReadableArray?) {
val valuesArray = values?.let { array ->
Array(array.size()) { index ->
array.getString(index) ?: ""
}
} ?: emptyArray()
view.values = valuesArray
}
@ReactProp(name = "selectedIndex", defaultInt = 0)
fun setSelectedIndex(view: CustomSegmentedControl, selectedIndex: Int) {
view.selectedIndex = selectedIndex
}
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any>? {
return MapBuilder.builder<String, Any>()
.put(ON_CHANGE_EVENT, MapBuilder.of("registrationName", ON_CHANGE_EVENT))
.build()
}
override fun onAfterUpdateTransaction(view: CustomSegmentedControl) {
super.onAfterUpdateTransaction(view)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -3,16 +3,14 @@
buildscript {
ext {
minSdkVersion = 24
buildToolsVersion = "35.0.0"
compileSdkVersion = 35
targetSdkVersion = 35
buildToolsVersion = "36.0.0"
compileSdkVersion = 36
targetSdkVersion = 36
googlePlayServicesVersion = "16.+"
googlePlayServicesIidVersion = "16.0.1"
firebaseVersion = "17.3.4"
firebaseMessagingVersion = "21.1.0"
ndkVersion = "27.1.12297006"
kotlin_version = '2.0.21'
kotlinVersion = '2.0.21'
firebaseVersion = "21.1.0"
ndkVersion = "28.2.13676358"
kotlinVersion = '2.1.20'
}
repositories {
google()
@ -21,17 +19,51 @@ buildscript {
dependencies {
classpath("com.android.tools.build:gradle")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
classpath 'com.google.gms:google-services:4.4.4' // Google Services plugin
classpath("com.bugsnag:bugsnag-android-gradle-plugin:8.2.0")
}
}
// Gradle 9 removes jcenter(); add a shim that redirects any jcenter() call to mavenCentral()
def addJcenterShim = { repoContainer, logger ->
def mc = repoContainer.metaClass
if (!mc.respondsTo(repoContainer, 'jcenter')) {
mc.jcenter << { Closure config = null ->
def repo = repoContainer.mavenCentral()
if (config != null) {
config.delegate = repo
config.resolveStrategy = Closure.DELEGATE_FIRST
config.call(repo)
}
logger.lifecycle("Redirected jcenter() to mavenCentral()")
return repo
}
}
if (!mc.respondsTo(repoContainer, 'methodMissing')) {
mc.methodMissing = { String name, args ->
if (name == 'jcenter') {
def repo = repoContainer.mavenCentral()
logger.lifecycle("Redirected jcenter() (methodMissing) to mavenCentral()")
return repo
}
throw new MissingMethodException(name, repoContainer.class, args)
}
}
}
allprojects {
repositories {
maven {
url("$rootDir/../node_modules/detox/Detox-android")
}
// react-native-background-fetch ships com.transistorsoft:tsbackgroundfetch
// as a bundled local Maven repo; the package's own build.gradle adds it
// for itself, but :app's runtime classpath resolution needs it visible
// at the root level too.
maven {
url("$rootDir/../node_modules/react-native-background-fetch/android/libs")
}
mavenCentral {
// We don't want to fetch react-native from Maven Central as there are
@ -40,26 +72,66 @@ allprojects {
excludeGroup "com.facebook.react"
}
}
mavenLocal()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url("$rootDir/../node_modules/react-native/android")
}
maven {
// Android JSC is installed from npm
url("$rootDir/../node_modules/jsc-android/dist")
}
google()
maven { url 'https://www.jitpack.io' }
}
}
subprojects {
afterEvaluate {project ->
if (project.hasProperty("android")) {
// Apply jcenter shim very early for every project before its build.gradle is evaluated
gradle.beforeProject { project ->
addJcenterShim(project.repositories, project.logger)
if (project.buildscript != null) {
addJcenterShim(project.buildscript.repositories, project.logger)
}
}
// Apply to root as well
addJcenterShim(repositories, logger)
if (buildscript != null) {
addJcenterShim(buildscript.repositories, logger)
}
subprojects { project ->
// react-native-device-info's androidTest classpath pulls
// play-services-iid:16.0.1 -> play-services-base:16.0.1 -> support-v4:26.1.0,
// which collides with androidx.core:core:1.13.1 (Duplicate class
// android.support.v4.app.INotificationSideChannel). Exclude the pre-AndroidX
// support-* modules so the AndroidX equivalents in core win.
configurations.all {
exclude group: 'com.android.support', module: 'support-compat'
exclude group: 'com.android.support', module: 'support-annotations'
exclude group: 'com.android.support', module: 'support-core-utils'
}
// Remove and block any jcenter() repositories at both project and buildscript levels
def scrub = { repoContainer ->
repoContainer.all { repo ->
if (repo instanceof org.gradle.api.artifacts.repositories.MavenArtifactRepository &&
repo.url?.toString()?.contains('jcenter')) {
project.logger.lifecycle("Removing jcenter() from ${project.path}")
repoContainer.remove(repo)
}
}
repoContainer.whenObjectAdded { repo ->
if (repo instanceof org.gradle.api.artifacts.repositories.MavenArtifactRepository &&
repo.url?.toString()?.contains('jcenter')) {
project.logger.lifecycle("Blocking jcenter() from ${project.path}")
repoContainer.remove(repo)
repoContainer.mavenCentral()
}
}
}
scrub(project.repositories)
if (project.buildscript != null) {
scrub(project.buildscript.repositories)
}
afterEvaluate {proj ->
if (proj.hasProperty("android")) {
android {
buildToolsVersion "35.0.0"
compileSdkVersion 35
buildToolsVersion "36.0.0"
compileSdkVersion 36
defaultConfig {
minSdkVersion 24
}
@ -68,12 +140,32 @@ subprojects {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {
// FIXME: next line should be removed when https://github.com/wix/Detox/issues/4678 is fixed
kotlinOptions.freeCompilerArgs += ["-Xopt-in=kotlin.ExperimentalStdlibApi"]
if (project.plugins.hasPlugin("com.android.application") || project.plugins.hasPlugin("com.android.library")) {
if (proj.plugins.hasPlugin("com.android.application") || proj.plugins.hasPlugin("com.android.library")) {
kotlinOptions.jvmTarget = android.compileOptions.sourceCompatibility
} else {
kotlinOptions.jvmTarget = sourceCompatibility
}
}
if (proj.name == "react-native-reanimated" && proj.hasProperty("android")) {
// Wire Reanimated's generated codegen sources so WorkletsModule spec is visible under new architecture
proj.android.sourceSets.main.java.srcDir("${proj.buildDir}/generated/source/codegen/java")
}
}
}
}
// Final guard: fail fast if any jcenter repository slips through
gradle.projectsEvaluated {
allprojects { proj ->
def offenders = proj.repositories.findAll { repo ->
repo instanceof org.gradle.api.artifacts.repositories.MavenArtifactRepository &&
repo.url?.toString()?.contains('jcenter')
}
if (!offenders.isEmpty()) {
throw new GradleException("jcenter() detected in ${proj.path}; remove or replace with mavenCentral()");
}
}
}
apply plugin: "com.facebook.react.rootproject"

View File

@ -21,7 +21,6 @@ org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
android.enableJetifier=true
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
@ -32,8 +31,16 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
newArchEnabled=true
# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
hermesEnabled=true
hermesEnabled=true
# Use this property to enable edge-to-edge display support.
# This allows your app to draw behind system bars for an immersive UI.
# Note: Only works with ReactActivity and should not be used with custom Activity.
edgeToEdgeEnabled=true
# Use legacy NDK symbol upload for Bugsnag (avoids v5.26.0 requirement)
bugsnag.useLegacyNdkSymbolUpload=true

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

6
android/gradlew vendored
View File

@ -114,8 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
@ -172,7 +170,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@ -212,8 +209,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

4
android/gradlew.bat vendored
View File

@ -70,11 +70,9 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

View File

@ -1,4 +1,7 @@
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['react-native-reanimated/plugin'], // required by react-native-reanimated v2 https://docs.swmansion.com/react-native-reanimated/docs/installation/
// Pin the @babel/runtime version so Metro resolves a single copy instead of
// bundling duplicate helpers, which bloats the bundle.
// See https://github.com/babel/babel/issues/18050
presets: [['module:@react-native/babel-preset', { enableBabelRuntime: '^7.26.0' }]],
plugins: ['react-native-worklets/plugin'],
};

View File

@ -5,7 +5,10 @@ import RNFS from 'react-native-fs';
import Realm from 'realm';
import { sha256 as _sha256 } from '@noble/hashes/sha256';
import { LegacyWallet, SegwitBech32Wallet, SegwitP2SHWallet, TaprootWallet } from '../class';
import type { LegacyWallet as LegacyWalletT } from '../class/wallets/legacy-wallet';
import type { SegwitBech32Wallet as SegwitBech32WalletT } from '../class/wallets/segwit-bech32-wallet';
import type { SegwitP2SHWallet as SegwitP2SHWalletT } from '../class/wallets/segwit-p2sh-wallet';
import type { TaprootWallet as TaprootWalletT } from '../class/wallets/taproot-wallet';
import presentAlert from '../components/Alert';
import loc from '../loc';
import { GROUP_IO_BLUEWALLET } from './currency';
@ -27,7 +30,7 @@ type Utxo = {
wif?: string;
};
type ElectrumTransaction = {
export type ElectrumTransaction = {
txid: string;
hash: string;
version: number;
@ -55,13 +58,14 @@ type ElectrumTransaction = {
addresses: string[];
};
}[];
blockhash: string;
confirmations: number;
time: number;
blocktime: number;
// Confirmation-only fields: absent on mempool (unconfirmed) responses.
blockhash?: string;
confirmations?: number;
time?: number;
blocktime?: number;
};
type ElectrumTransactionWithHex = ElectrumTransaction & {
export type ElectrumTransactionWithHex = ElectrumTransaction & {
hex: string;
};
@ -90,7 +94,6 @@ export const hardcodedPeers: Peer[] = [
// { host: 'electrum.jochen-hoenicke.de', ssl: '50006' },
{ host: 'electrum1.bluewallet.io', ssl: 443 },
{ host: 'electrum.acinq.co', ssl: 50002 },
{ host: 'electrum.bitaroo.net', ssl: 50002 },
];
export const suggestedServers: Peer[] = hardcodedPeers.map(peer => ({
@ -98,13 +101,84 @@ export const suggestedServers: Peer[] = hardcodedPeers.map(peer => ({
}));
let mainClient: typeof ElectrumClient | undefined;
let mainConnected: boolean = false;
let wasConnectedAtLeastOnce: boolean = false;
let serverName: string | false = false;
let disableBatching: boolean = false;
let connectionAttempt: number = 0;
let currentPeerIndex = Math.floor(Math.random() * hardcodedPeers.length);
let currentPeerIndex = hardcodedPeers.findIndex(peer => peer.host === defaultPeer.host && peer.ssl === defaultPeer.ssl);
if (currentPeerIndex < 0) currentPeerIndex = 0;
let latestBlock: { height: number; time: number } | { height: undefined; time: undefined } = { height: undefined, time: undefined };
// --- Single source of truth for connection liveness -----------------------------
// We previously tracked `mainConnected` (boolean) separately from the client's own
// `mainClient.status`. They drifted on iOS suspend/resume: a transient `ping()`
// failure cleared the flag while the socket was still alive, then `waitTillConnected`
// blocked for ~30s on the stale flag and surfaced a false network-error alert. The
// state machine + `ensureConnected()` below is the only place that mutates the
// connection lifecycle, and UI is driven by subscribing to state changes.
export type ConnectionState = 'disabled' | 'disconnected' | 'connecting' | 'connected';
let connState: ConnectionState = 'disconnected';
type ConnectionListener = (state: ConnectionState) => void;
const connectionListeners = new Set<ConnectionListener>();
function setConnectionState(next: ConnectionState): void {
if (connState === next) return;
connState = next;
for (const l of connectionListeners) {
try {
l(next);
} catch (e) {
console.warn('[electrum] connection listener threw:', e);
}
}
}
/** Current connection state for UI. */
export function getConnectionState(): ConnectionState {
return connState;
}
/** Subscribe to state changes. Returns an unsubscribe function. */
export function subscribeConnectionState(listener: ConnectionListener): () => void {
connectionListeners.add(listener);
return () => {
connectionListeners.delete(listener);
};
}
/** Convenience: `true` iff a usable Electrum connection is currently believed to exist. */
export function isConnected(): boolean {
return connState === 'connected';
}
// --- Connection lifecycle internals ---------------------------------------------
/** One liveness check (`server_ping`) wall-time before giving up and marking the socket dead. */
const PING_TIMEOUT_MS = 5_000;
/** One full connect attempt (TLS + `server_version` handshake) wall-time before retrying. */
const CONNECT_ATTEMPT_TIMEOUT_MS = 10_000;
/** Reconnect attempts inside a single `ensureConnected()` call before declaring failure. */
const CONNECT_MAX_ATTEMPTS = 5;
/** Backoff between attempts to avoid hammering a flaky server. */
const CONNECT_BACKOFF_MS = 500;
/** Delay before the auto-reconnect triggered by a live-socket `onError`. Onions are slower. */
const RECONNECT_ONION_DELAY_MS = 4_000;
const RECONNECT_TCP_DELAY_MS = 500;
/** Max wall time one `ensureConnected()` call may take when no live socket exists. */
export const ENSURE_CONNECTED_MAX_WALL_MS =
CONNECT_MAX_ATTEMPTS * CONNECT_ATTEMPT_TIMEOUT_MS + (CONNECT_MAX_ATTEMPTS - 1) * CONNECT_BACKOFF_MS;
/** Coalesces concurrent `ensureConnected()` callers — at most one connect attempt at a time. */
let ensureInFlight: Promise<boolean> | null = null;
/** If any coalesced caller asked for the failure alert, honour it once the in-flight attempt finishes. */
let ensureInFlightShowAlert = false;
/**
* Bumps every time the caller asks us to abandon the current connection
* (`forceDisconnect()` or user disabling Electrum). In-flight `ensureConnected()`
* checks this between attempts so it can bail out promptly instead of racing back
* to `connected` after a disconnect was requested.
*/
let disconnectGeneration = 0;
const txhashHeightCache: Record<string, number> = {};
let _realm: Realm | undefined;
@ -150,10 +224,10 @@ export const getPreferredServer = async (): Promise<ElectrumServerItem | undefin
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
console.log('Getting preferred server:', { host, tcpPort, sslPort });
console.log('[electrum] Getting preferred server:', { host, tcpPort, sslPort });
if (!host) {
console.warn('Preferred server host is undefined');
console.warn('[electrum] Preferred server host is undefined');
return;
}
@ -163,7 +237,7 @@ export const getPreferredServer = async (): Promise<ElectrumServerItem | undefin
ssl: sslPort ? Number(sslPort) : undefined,
};
} catch (error) {
console.error('Error in getPreferredServer:', error);
console.error('[electrum] Error in getPreferredServer:', error);
return undefined;
}
};
@ -171,12 +245,12 @@ export const getPreferredServer = async (): Promise<ElectrumServerItem | undefin
export const removePreferredServer = async () => {
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
console.log('Removing preferred server');
console.log('[electrum] Removing preferred server');
await DefaultPreference.clear(ELECTRUM_HOST);
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
} catch (error) {
console.error('Error in removePreferredServer:', error);
console.error('[electrum] Error in removePreferredServer:', error);
}
};
@ -185,14 +259,14 @@ export async function isDisabled(): Promise<boolean> {
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const savedValue = await DefaultPreference.get(ELECTRUM_CONNECTION_DISABLED);
console.log('Getting Electrum connection disabled state:', savedValue);
console.log('[electrum] Getting Electrum connection disabled state:', savedValue);
if (savedValue === null) {
result = false;
} else {
result = savedValue;
}
} catch (error) {
console.error('Error getting Electrum connection disabled state:', error);
console.error('[electrum] Error getting Electrum connection disabled state:', error);
result = false;
}
return !!result;
@ -200,8 +274,23 @@ export async function isDisabled(): Promise<boolean> {
export async function setDisabled(disabled = true) {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
console.log('Setting Electrum connection disabled state to:', disabled);
return DefaultPreference.set(ELECTRUM_CONNECTION_DISABLED, disabled ? '1' : '');
console.log('[electrum] Setting Electrum connection disabled state to:', disabled);
const result = await DefaultPreference.set(ELECTRUM_CONNECTION_DISABLED, disabled ? '1' : '');
// Disabling must abort any in-flight ensureConnected() and tear down the live
// socket so callers don't have to remember to pair this with forceDisconnect().
// Without bumping the generation, an in-flight connect could race back to
// 'connected' after the user toggled Electrum off.
if (disabled) {
disconnectGeneration += 1;
if (mainClient) {
try {
mainClient.close();
} catch {}
mainClient = undefined;
}
setConnectionState('disabled');
}
return result;
}
function getCurrentPeer() {
@ -214,7 +303,7 @@ function getCurrentPeer() {
function getNextPeer() {
const peer = getCurrentPeer();
currentPeerIndex++;
if (currentPeerIndex + 1 >= hardcodedPeers.length) currentPeerIndex = 0;
if (currentPeerIndex >= hardcodedPeers.length) currentPeerIndex = 0;
return peer;
}
@ -225,7 +314,7 @@ async function getSavedPeer(): Promise<Peer | null> {
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
console.log('Getting saved peer:', { host, tcpPort, sslPort });
console.log('[electrum] Getting saved peer:', { host, tcpPort, sslPort });
if (!host) {
return null;
@ -241,53 +330,98 @@ async function getSavedPeer(): Promise<Peer | null> {
return null;
} catch (error) {
console.error('Error in getSavedPeer:', error);
console.error('[electrum] Error in getSavedPeer:', error);
return null;
}
}
export async function connectMain(): Promise<void> {
if (await isDisabled()) {
console.log('Electrum connection disabled by user. Skipping connectMain call');
return;
}
/** Resolve to the peer this attempt should target (preferred saved peer, or rotate hardcoded list). */
async function pickPeer(): Promise<Peer> {
let usingPeer = getNextPeer();
const savedPeer = await getSavedPeer();
if (savedPeer && savedPeer.host && (savedPeer.tcp || savedPeer.ssl)) {
usingPeer = savedPeer;
}
return usingPeer;
}
console.log('Using peer:', JSON.stringify(usingPeer));
function scheduleReconnectFromClient(client: typeof ElectrumClient, usingPeer: Peer, reason: string): void {
if (connState !== 'connected' || mainClient !== client) return;
console.log(`[electrum] scheduling Electrum reconnect after ${reason}`);
try {
// Also neutralises electrum-client's own timers/reconnect hooks for this instance.
client.close();
} catch {}
if (mainClient === client) mainClient = undefined;
setConnectionState('disconnected');
const delay = usingPeer.host.endsWith('.onion') ? RECONNECT_ONION_DELAY_MS : RECONNECT_TCP_DELAY_MS;
const generationAtSchedule = disconnectGeneration;
setTimeout(() => {
if (generationAtSchedule !== disconnectGeneration) return;
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- defined later in file
ensureConnected().catch(() => {
/* ensureConnected never throws, but be defensive */
});
}, delay);
}
/**
* One connect attempt: build a fresh `ElectrumClient`, run the version handshake,
* subscribe to headers. No retries, no UI side effects. Returns the peer used
* (for caller-side telemetry/alerts) and whether the attempt succeeded.
*/
async function attemptConnectOnce(): Promise<{ ok: boolean; peer: Peer }> {
const usingPeer = await pickPeer();
console.log('[electrum] Using peer:', JSON.stringify(usingPeer));
// Drop any prior client before allocating a new one. Closing also neutralises
// electrum-client's internal `reconnect()` loop on the old instance.
if (mainClient) {
try {
mainClient.close();
} catch {}
mainClient = undefined;
}
try {
console.log('begin connection:', JSON.stringify(usingPeer));
mainClient = new ElectrumClient(net, tls, usingPeer.ssl || usingPeer.tcp, usingPeer.host, usingPeer.ssl ? 'tls' : 'tcp');
console.log('[electrum] begin connection:', JSON.stringify(usingPeer));
const client = new ElectrumClient(net, tls, usingPeer.ssl || usingPeer.tcp, usingPeer.host, usingPeer.ssl ? 'tls' : 'tcp');
mainClient = client;
mainClient.onError = function (e: { message: string }) {
console.log('electrum mainClient.onError():', e.message);
if (mainConnected) {
// most likely got a timeout from electrum ping. lets reconnect
// but only if we were previously connected (mainConnected), otherwise theres other
// code which does connection retries
mainClient?.close();
mainClient = undefined;
mainConnected = false;
// dropping `mainConnected` flag ensures there wont be reconnection race condition if several
// errors triggered
console.log('reconnecting after socket error');
setTimeout(connectMain, usingPeer.host.endsWith('.onion') ? 4000 : 500);
}
// Live-socket errors after a successful handshake: schedule a single
// `ensureConnected()` (deduped). Errors during this attempt's own handshake
// are caught below — we must not double-handle them here.
client.onError = function (e: { message: string }) {
console.log('[electrum] electrum mainClient.onError():', e.message);
scheduleReconnectFromClient(client, usingPeer, 'socket error');
};
const ver = await mainClient.initElectrum({ client: 'bluewallet', version: '1.4' });
const ver = await Promise.race([
client.initElectrum(
{ client: 'bluewallet', version: '1.4' },
{
maxRetry: 0,
callback: () => scheduleReconnectFromClient(client, usingPeer, 'socket close'),
},
),
new Promise<never>((_resolve, reject) => setTimeout(() => reject(new Error('connect timeout')), CONNECT_ATTEMPT_TIMEOUT_MS)),
]);
if (mainClient !== client) {
// Caller raced `forceDisconnect()` while we were awaiting. Bail.
try {
client.close();
} catch {}
return { ok: false, peer: usingPeer };
}
if (ver && ver[0]) {
console.log('connected to ', ver);
console.log('[electrum] connected to ', ver);
serverName = ver[0];
mainConnected = true;
wasConnectedAtLeastOnce = true;
if (ver[0].startsWith('ElectrumPersonalServer') || ver[0].startsWith('electrs') || ver[0].startsWith('Fulcrum')) {
disableBatching = true;
// exeptions for versions:
const [electrumImplementation, electrumVersion] = ver[0].split(' ');
switch (electrumImplementation) {
case 'electrs':
@ -296,8 +430,6 @@ export async function connectMain(): Promise<void> {
}
break;
case 'electrs-esplora':
// its a different one, and it does NOT support batching
// nop
break;
case 'Fulcrum':
if (semVerToInt(electrumVersion) >= semVerToInt('1.9.0')) {
@ -306,35 +438,154 @@ export async function connectMain(): Promise<void> {
break;
}
}
const header = await mainClient.blockchainHeaders_subscribe();
const header = await client.blockchainHeaders_subscribe();
if (header && header.height) {
latestBlock = {
height: header.height,
time: Math.floor(+new Date() / 1000),
};
}
// AsyncStorage.setItem(storageKey, JSON.stringify(peers)); TODO: refactor
return { ok: true, peer: usingPeer };
}
return { ok: false, peer: usingPeer };
} catch (e) {
mainConnected = false;
console.log('bad connection:', JSON.stringify(usingPeer), e);
mainClient?.close();
mainClient = undefined;
console.log('[electrum] bad connection:', JSON.stringify(usingPeer), e);
if (mainClient) {
try {
mainClient.close();
} catch {}
mainClient = undefined;
}
return { ok: false, peer: usingPeer };
}
}
/** Single liveness check on the current `mainClient`, bounded by `PING_TIMEOUT_MS`. */
async function pingWithTimeout(timeoutMs: number = PING_TIMEOUT_MS): Promise<boolean> {
if (!mainClient) return false;
const client = mainClient;
try {
await Promise.race([
client.server_ping(),
new Promise<never>((_resolve, reject) => setTimeout(() => reject(new Error('ping timeout')), timeoutMs)),
]);
return mainClient === client; // server replied AND client wasn't swapped while we waited
} catch {
return false;
}
}
export type EnsureConnectedOptions = {
/**
* Show the legacy "couldn't connect" alert (Try again / Reset / Cancel) on failure.
* Used by initial bootstrap (`SettingsProvider` re-enabling Electrum) and the manual
* help alert. Off-hot-path callers (refresh, broadcast, etc.) should leave this false
* and surface their own UI.
*/
showAlertOnFailure?: boolean;
};
/**
* Make sure a usable Electrum connection exists, healing if needed.
*
* - If we already think we're connected, run one fast `ping` to verify. If the ping
* succeeds, we're done. If it fails the client is torn down and we fall through
* to a reconnect.
* - Otherwise run up to `CONNECT_MAX_ATTEMPTS` connect attempts (each with its own
* timeout + backoff).
*
* Concurrent callers share the same in-flight promise there is at most one connect
* attempt at a time per process. This replaces the old `mainConnected`-flag-polling
* `waitTillConnected()`, which could block ~30s on a stale flag while the socket was
* still alive.
*/
export async function ensureConnected(opts: EnsureConnectedOptions = {}): Promise<boolean> {
const { showAlertOnFailure = false } = opts;
if (await isDisabled()) {
setConnectionState('disabled');
return false;
}
if (!mainConnected) {
console.log('retry');
connectionAttempt = connectionAttempt + 1;
mainClient?.close();
mainClient = undefined;
if (connectionAttempt >= 5) {
presentNetworkErrorAlert(usingPeer);
} else {
console.log('reconnection attempt #', connectionAttempt);
await new Promise(resolve => setTimeout(resolve, 500)); // sleep
return connectMain();
}
if (ensureInFlight) {
if (showAlertOnFailure) ensureInFlightShowAlert = true;
return ensureInFlight;
}
ensureInFlightShowAlert = showAlertOnFailure;
ensureInFlight = (async (): Promise<boolean> => {
const myGeneration = disconnectGeneration;
/** True iff the current generation no longer matches ours (i.e. `forceDisconnect()` ran). */
const aborted = (where: string): boolean => {
if (myGeneration === disconnectGeneration) return false;
console.log(`[electrum] ensureConnected aborted by forceDisconnect at ${where} (gen ${myGeneration}${disconnectGeneration})`);
return true;
};
let lastPeer: Peer | undefined;
try {
// Fast path: live ping on the existing client.
if (mainClient && connState === 'connected') {
if (await pingWithTimeout()) {
// If a disconnect/disable raced us, the bumper already set the right
// state ('disconnected' or 'disabled'); don't clobber it from here.
if (aborted('post-ping')) return false;
return true;
}
// Stale socket. Tear it down so the attempt loop starts fresh.
try {
mainClient.close();
} catch {}
mainClient = undefined;
setConnectionState('disconnected');
}
if (aborted('pre-loop')) return false;
setConnectionState('connecting');
for (let i = 0; i < CONNECT_MAX_ATTEMPTS; i++) {
if (await isDisabled()) {
setConnectionState('disabled');
return false;
}
// Generation-bumper (`forceDisconnect` or `setDisabled(true)`) already
// set the appropriate terminal state; we must not clobber 'disabled'
// back to 'disconnected' here.
if (aborted(`attempt ${i} start`)) return false;
const { ok, peer } = await attemptConnectOnce();
lastPeer = peer;
if (aborted(`attempt ${i} end`)) {
if (mainClient) {
try {
mainClient.close();
} catch {}
mainClient = undefined;
}
return false;
}
if (ok) {
setConnectionState('connected');
return true;
}
if (i < CONNECT_MAX_ATTEMPTS - 1) {
await new Promise(resolve => setTimeout(resolve, CONNECT_BACKOFF_MS));
}
}
setConnectionState('disconnected');
if (ensureInFlightShowAlert) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- defined later in file
presentNetworkErrorAlert(lastPeer);
}
return false;
} finally {
ensureInFlight = null;
ensureInFlightShowAlert = false;
}
})();
return ensureInFlight;
}
export async function presentResetToDefaultsAlert(): Promise<boolean> {
@ -356,7 +607,7 @@ export async function presentResetToDefaultsAlert(): Promise<boolean> {
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
} catch (e) {
console.log(e); // Must be running on Android
console.log('[electrum]', e); // Must be running on Android
}
resolve(true);
},
@ -375,7 +626,7 @@ export async function presentResetToDefaultsAlert(): Promise<boolean> {
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
} catch (e) {
console.log(e); // Must be running on Android
console.log('[electrum]', e); // Must be running on Android
}
resolve(true);
},
@ -398,16 +649,16 @@ export async function presentResetToDefaultsAlert(): Promise<boolean> {
});
}
const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
async function presentNetworkErrorAlert(usingPeer?: Peer, allowRepeat = false) {
if (await isDisabled()) {
console.log(
'Electrum connection disabled by user. Perhaps we are attempting to show this network error alert after the user disabled connections.',
'[electrum] Electrum connection disabled by user. Perhaps we are attempting to show this network error alert after the user disabled connections.',
);
return;
}
presentAlert({
allowRepeat: false,
allowRepeat,
title: loc.errors.network,
message: loc.formatString(
usingPeer ? loc.settings.electrum_unable_to_connect : loc.settings.electrum_error_connect,
@ -417,10 +668,10 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
{
text: loc.wallets.list_tryagain,
onPress: () => {
connectionAttempt = 0;
mainClient?.close();
mainClient = undefined;
setTimeout(connectMain, 500);
forceDisconnect();
setTimeout(() => {
ensureConnected({ showAlertOnFailure: true }).catch(() => {});
}, 500);
},
style: 'default',
},
@ -429,10 +680,10 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
onPress: () => {
presentResetToDefaultsAlert().then(result => {
if (result) {
connectionAttempt = 0;
mainClient?.close();
mainClient = undefined;
setTimeout(connectMain, 500);
forceDisconnect();
setTimeout(() => {
ensureConnected({ showAlertOnFailure: true }).catch(() => {});
}, 500);
}
});
},
@ -441,16 +692,21 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
{
text: loc._.cancel,
onPress: () => {
connectionAttempt = 0;
mainClient?.close();
mainClient = undefined;
forceDisconnect();
},
style: 'cancel',
},
],
options: { cancelable: false },
});
};
}
/**
* Wallets list header when Electrum looks disconnected: same actions as the internal timeout alert, with allowRepeat so the user can open it again after dismiss.
*/
export async function presentElectrumDisconnectedHelpAlert(): Promise<void> {
await presentNetworkErrorAlert(undefined, true);
}
/**
* Returns random electrum server out of list of servers
@ -497,18 +753,27 @@ export const getBalanceByAddress = async function (address: string): Promise<{ c
balance.addr = address;
return balance;
} catch (error) {
console.error('Error in getBalanceByAddress:', error);
console.error('[electrum] Error in getBalanceByAddress:', error);
throw error;
}
};
export const getConfig = async function () {
if (!mainClient) throw new Error('Electrum client is not connected');
if (!mainClient) {
return {
host: undefined,
port: undefined,
serverName: false as typeof serverName,
connected: connState === 'connected' ? 1 : 0,
};
}
return {
host: mainClient.host,
port: mainClient.port,
serverName,
connected: mainClient.timeLastCall !== 0 && mainClient.status,
// Drive UI "connected" indicator from the single state machine so the settings
// screen agrees with the wallets-list header pill and with `ensureConnected()`.
connected: connState === 'connected' ? 1 : 0,
};
};
@ -537,14 +802,24 @@ export const getMempoolTransactionsByAddress = async function (address: string):
return mainClient.blockchainScripthash_getMempool(uint8ArrayToHex(reversedHash));
};
export const ping = async function () {
try {
await mainClient.server_ping();
return true;
} catch (_) {}
mainConnected = false;
return false;
/**
* Read-only liveness probe. Does NOT trigger reconnects (use `ensureConnected()`
* for that). Updates the connection state machine to reflect the probe result so
* subscribers (UI pill, settings screen) stay in sync.
*
* - `true`: server replied within `PING_TIMEOUT_MS`.
* - `false`: client missing, timed out, or server errored.
*/
export const ping = async function (): Promise<boolean> {
if (await isDisabled()) return false;
const ok = await pingWithTimeout();
if (ok) {
// Heal stale `disconnected` state from a transient ping failure earlier.
if (connState !== 'connected') setConnectionState('connected');
} else if (connState === 'connected') {
setConnectionState('disconnected');
}
return ok;
};
// exported only to be used in unit tests
@ -599,6 +874,21 @@ export function txhexToElectrumTransaction(txhex: string): ElectrumTransactionWi
let address: false | string = false;
let type: false | string = false;
// Lazy require to avoid the module-scope cycle described above. These
// modules are fully loaded by the time this function is actually invoked.
const { SegwitBech32Wallet } = require('../class/wallets/segwit-bech32-wallet') as {
SegwitBech32Wallet: typeof SegwitBech32WalletT;
};
const { SegwitP2SHWallet } = require('../class/wallets/segwit-p2sh-wallet') as {
SegwitP2SHWallet: typeof SegwitP2SHWalletT;
};
const { LegacyWallet } = require('../class/wallets/legacy-wallet') as {
LegacyWallet: typeof LegacyWalletT;
};
const { TaprootWallet } = require('../class/wallets/taproot-wallet') as {
TaprootWallet: typeof TaprootWalletT;
};
if (SegwitBech32Wallet.scriptPubKeyToAddress(uint8ArrayToHex(out.script))) {
address = SegwitBech32Wallet.scriptPubKeyToAddress(uint8ArrayToHex(out.script));
type = 'witness_v0_keyhash';
@ -742,7 +1032,7 @@ export const multiGetBalanceByAddress = async (addresses: string[], batchsize: n
}
for (const bal of balances) {
if (bal.error) console.warn('multiGetBalanceByAddress():', bal.error);
if (bal.error) console.warn('[electrum] multiGetBalanceByAddress():', bal.error);
ret.balance += +bal.result.confirmed;
ret.unconfirmed_balance += +bal.result.unconfirmed;
ret.addresses[scripthash2addr[bal.param]] = bal.result;
@ -836,7 +1126,7 @@ export const multiGetHistoryByAddress = async function (
}
for (const history of results) {
if (history.error) console.warn('multiGetHistoryByAddress():', history.error);
if (history.error) console.warn('[electrum] multiGetHistoryByAddress():', history.error);
ret[scripthash2addr[history.param]] = history.result || [];
for (const result of history.result || []) {
if (result.tx_hash) txhashHeightCache[result.tx_hash] = result.height; // cache tx height
@ -877,7 +1167,7 @@ export async function multiGetTransactionByTxid<T extends boolean>(
try {
ret[txid] = JSON.parse(jsonString.cache_value as string);
} catch (error) {
console.log(error, 'cache failed to parse', jsonString.cache_value);
console.log('[electrum]', error, 'cache failed to parse', jsonString.cache_value);
}
}
@ -927,7 +1217,7 @@ export async function multiGetTransactionByTxid<T extends boolean>(
tx = txhexToElectrumTransaction(tx);
results.push({ result: tx, param: txid });
} catch (err) {
console.log(err);
console.log('[electrum]', err);
}
}
} else {
@ -943,7 +1233,7 @@ export async function multiGetTransactionByTxid<T extends boolean>(
}
results.push({ result: tx, param: txid });
} catch (err) {
console.log(err);
console.log('[electrum]', err);
}
}
}
@ -998,41 +1288,12 @@ export async function multiGetTransactionByTxid<T extends boolean>(
}
});
} catch (writeError) {
console.error('Failed to write transaction cache:', writeError);
console.error('[electrum] Failed to write transaction cache:', writeError);
}
return ret;
}
/**
* Simple waiter till `mainConnected` becomes true (which means
* it Electrum was connected in other function), or timeout 30 sec.
*/
export const waitTillConnected = async function (): Promise<boolean> {
let waitTillConnectedInterval: NodeJS.Timeout | undefined;
let retriesCounter = 0;
if (await isDisabled()) {
console.warn('Electrum connections disabled by user. waitTillConnected skipping...');
return false;
}
return new Promise(function (resolve, reject) {
waitTillConnectedInterval = setInterval(() => {
if (mainConnected) {
clearInterval(waitTillConnectedInterval);
return resolve(true);
}
if (wasConnectedAtLeastOnce && retriesCounter++ >= 150) {
// `wasConnectedAtLeastOnce` needed otherwise theres gona be a race condition with the code that connects
// electrum during app startup
clearInterval(waitTillConnectedInterval);
presentNetworkErrorAlert();
reject(new Error('Waiting for Electrum connection timeout'));
}
}, 100);
});
};
// Returns the value at a given percentile in a sorted numeric array.
// "Linear interpolation between closest ranks" method
function percentile(arr: number[], p: number) {
@ -1184,10 +1445,11 @@ export const testConnection = async function (host: string, tcpPort?: number, ss
client.onError = () => {}; // mute
let timeoutId: NodeJS.Timeout | undefined;
const timeoutMs = host.endsWith('.onion') ? 21_000 : 5_000;
try {
const rez = await Promise.race([
new Promise(resolve => {
timeoutId = setTimeout(() => resolve('timeout'), 5000);
timeoutId = setTimeout(() => resolve('timeout'), timeoutMs);
}),
client.connect(),
]);
@ -1205,8 +1467,19 @@ export const testConnection = async function (host: string, tcpPort?: number, ss
return false;
};
/**
* Drop the current connection and tell any in-flight `ensureConnected()` to abort
* (so it doesn't race the disconnect by setting state back to `connected`).
*/
export const forceDisconnect = (): void => {
mainClient?.close();
disconnectGeneration += 1;
if (mainClient) {
try {
mainClient.close();
} catch {}
mainClient = undefined;
}
setConnectionState('disconnected');
};
export const setBatchingDisabled = () => {
@ -1217,6 +1490,10 @@ export const setBatchingEnabled = () => {
disableBatching = false;
};
export function getServerBanner(): Promise<string> {
return mainClient.request('server.banner', []);
}
const splitIntoChunks = function (arr: any[], chunkSize: number) {
const groups = [];
let i;

View File

@ -0,0 +1 @@
export { default, type Spec } from '../codegen/NativeEventEmitter';

View File

@ -0,0 +1 @@
export { default, type Spec } from '../codegen/NativeMenuElementsEmitter';

View File

@ -0,0 +1 @@
export { default, type Spec } from '../codegen/NativeWidgetHelper';

View File

@ -1,4 +1,5 @@
import { NativeModules, Platform } from 'react-native';
import { Platform } from 'react-native';
import NativeSettingsModule from '../codegen/NativeSettingsModule';
interface SettingsModuleInterface {
/**
@ -46,6 +47,7 @@ interface SettingsModuleInterface {
}
// Only available on Android
const SettingsModule: SettingsModuleInterface | null = Platform.OS === 'android' ? NativeModules.SettingsModule : null;
const nativeModule = NativeSettingsModule ?? null;
const SettingsModule: SettingsModuleInterface | null = Platform.OS === 'android' ? nativeModule : null;
export default SettingsModule;

View File

@ -9,13 +9,13 @@ import androidx.core.content.ContextCompat
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.events.Event
import com.google.android.material.button.MaterialButton
import com.google.android.material.button.MaterialButtonToggleGroup
import io.bluewallet.bluewallet.R
class CustomSegmentedControl @JvmOverloads constructor(
class SegmentedControl @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
@ -23,7 +23,11 @@ class CustomSegmentedControl @JvmOverloads constructor(
private val toggleGroup: MaterialButtonToggleGroup
private var currentSelectedIndex: Int = 0
private var onChangeEvent: ((WritableMap) -> Unit)? = null
private var backgroundColorProp: Int? = null
private var tintColorProp: Int? = null
private var textColorProp: Int? = null
private var momentaryProp: Boolean = false
private var isEnabledProp: Boolean = true
var values: Array<String> = emptyArray()
set(value) {
@ -44,10 +48,13 @@ class CustomSegmentedControl @JvmOverloads constructor(
isSingleSelection = true
isSelectionRequired = true
}
addView(toggleGroup, LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
))
addView(
toggleGroup,
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
),
)
toggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (isChecked) {
@ -55,6 +62,9 @@ class CustomSegmentedControl @JvmOverloads constructor(
if (newIndex != -1 && newIndex != currentSelectedIndex) {
currentSelectedIndex = newIndex
emitChangeEvent(newIndex)
if (momentaryProp) {
toggleGroup.clearChecked()
}
}
}
}
@ -62,28 +72,29 @@ class CustomSegmentedControl @JvmOverloads constructor(
private fun updateSegments() {
toggleGroup.removeAllViews()
values.forEachIndexed { index, title ->
val button = MaterialButton(
context,
null,
com.google.android.material.R.attr.materialButtonOutlinedStyle
com.google.android.material.R.attr.materialButtonOutlinedStyle,
).apply {
text = title
id = generateViewId()
layoutParams = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
1f,
)
isCheckable = true
strokeWidth = 2
applyEnabledState()
val cornerRadius = resources.getDimensionPixelSize(
com.google.android.material.R.dimen.mtrl_btn_corner_radius
com.google.android.material.R.dimen.mtrl_btn_corner_radius,
)
when {
values.size == 1 -> {
this.cornerRadius = cornerRadius
@ -99,10 +110,10 @@ class CustomSegmentedControl @JvmOverloads constructor(
}
}
}
toggleGroup.addView(button)
}
updateButtonColors()
updateSelectedSegment()
}
@ -110,54 +121,82 @@ class CustomSegmentedControl @JvmOverloads constructor(
private fun updateButtonColors() {
for (i in 0 until toggleGroup.childCount) {
val button = toggleGroup.getChildAt(i) as? MaterialButton ?: continue
val selectedBgColor = ContextCompat.getColor(context, R.color.button_background_color)
val unselectedBgColor = ContextCompat.getColor(context, R.color.button_disabled_background_color)
val selectedTextColor = ContextCompat.getColor(context, R.color.button_text_color)
val unselectedTextColor = ContextCompat.getColor(context, R.color.button_disabled_text_color)
val selectedBgColor = tintColorProp ?: ContextCompat.getColor(context, R.color.button_background_color)
val unselectedBgColor = backgroundColorProp ?: ContextCompat.getColor(context, R.color.button_disabled_background_color)
val resolvedTextColor = textColorProp ?: ContextCompat.getColor(context, R.color.button_text_color)
val selectedTextColor = resolvedTextColor
val unselectedTextColor = textColorProp ?: ContextCompat.getColor(context, R.color.button_disabled_text_color)
val borderColor = ContextCompat.getColor(context, R.color.form_border_color)
val rippleColor = ContextCompat.getColor(context, R.color.ripple_color)
val rippleColorSelected = ContextCompat.getColor(context, R.color.ripple_color_selected)
val bgColorStateList = ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)
intArrayOf(-android.R.attr.state_checked),
),
intArrayOf(selectedBgColor, unselectedBgColor)
intArrayOf(selectedBgColor, unselectedBgColor),
)
val textColorStateList = ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked),
),
intArrayOf(selectedTextColor, unselectedTextColor)
intArrayOf(selectedTextColor, unselectedTextColor),
)
val strokeColorStateList = ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked),
),
intArrayOf(borderColor, borderColor)
intArrayOf(borderColor, borderColor),
)
val rippleColorStateList = ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked),
),
intArrayOf(rippleColorSelected, rippleColor)
intArrayOf(rippleColorSelected, rippleColor),
)
button.backgroundTintList = bgColorStateList
button.setTextColor(textColorStateList)
button.strokeColor = strokeColorStateList
button.rippleColor = rippleColorStateList
button.isEnabled = isEnabledProp
}
}
fun setBackgroundColorProp(color: String?) {
backgroundColorProp = parseColor(color)
updateButtonColors()
}
fun setTintColorProp(color: String?) {
tintColorProp = parseColor(color)
updateButtonColors()
}
fun setTextColorProp(color: String?) {
textColorProp = parseColor(color)
updateButtonColors()
}
fun setMomentaryProp(momentary: Boolean) {
momentaryProp = momentary
toggleGroup.isSelectionRequired = !momentary
}
fun setEnabledProp(enabled: Boolean) {
isEnabledProp = enabled
toggleGroup.isEnabled = enabled
applyEnabledState()
}
private fun updateSelectedSegment() {
if (values.isNotEmpty() && currentSelectedIndex in 0 until values.size) {
val buttonId = getButtonIdAtIndex(currentSelectedIndex)
@ -188,26 +227,37 @@ class CustomSegmentedControl @JvmOverloads constructor(
val reactContext = context as? ReactContext ?: return
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
val event = Arguments.createMap().apply {
putInt("selectedIndex", selectedIndex)
}
eventDispatcher?.dispatchEvent(ChangeEvent(surfaceId, id, event))
}
private fun applyEnabledState() {
for (i in 0 until toggleGroup.childCount) {
val button = toggleGroup.getChildAt(i) as? MaterialButton ?: continue
button.isEnabled = isEnabledProp
}
}
private fun parseColor(color: String?): Int? {
return try {
color?.let { Color.parseColor(it) }
} catch (_: IllegalArgumentException) {
null
}
}
private inner class ChangeEvent(
surfaceId: Int,
viewId: Int,
private val eventData: WritableMap
private val eventData: WritableMap,
) : Event<ChangeEvent>(surfaceId, viewId) {
override fun getEventName(): String = "onChangeEvent"
override fun getEventName(): String = "topChange"
override fun getEventData(): WritableMap = eventData
}
fun setOnChangeEvent(callback: ((WritableMap) -> Unit)?) {
onChangeEvent = callback
}
}
}

View File

@ -0,0 +1,67 @@
package io.bluewallet.bluewallet.components.segmentedcontrol
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.common.MapBuilder
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
@ReactModule(name = SegmentedControlManager.REACT_CLASS)
class SegmentedControlManager : SimpleViewManager<SegmentedControl>() {
companion object {
const val REACT_CLASS = "SegmentedControl"
}
override fun getName(): String = REACT_CLASS
override fun createViewInstance(reactContext: ThemedReactContext): SegmentedControl =
SegmentedControl(reactContext)
@ReactProp(name = "values")
fun setValues(view: SegmentedControl, values: ReadableArray?) {
view.values = values?.let { arr -> Array(arr.size()) { arr.getString(it) ?: "" } } ?: emptyArray()
}
@ReactProp(name = "selectedIndex", defaultInt = 0)
fun setSelectedIndex(view: SegmentedControl, selectedIndex: Int) {
view.selectedIndex = selectedIndex
}
@ReactProp(name = "enabled", defaultBoolean = true)
fun setEnabled(view: SegmentedControl, enabled: Boolean) {
view.setEnabledProp(enabled)
}
@ReactProp(name = "momentary", defaultBoolean = false)
fun setMomentary(view: SegmentedControl, momentary: Boolean) {
view.setMomentaryProp(momentary)
}
@ReactProp(name = "backgroundColor")
fun setBackgroundColor(view: SegmentedControl, backgroundColor: String?) {
view.setBackgroundColorProp(backgroundColor)
}
@ReactProp(name = "tintColor")
fun setTintColor(view: SegmentedControl, tintColor: String?) {
view.setTintColorProp(tintColor)
}
@ReactProp(name = "textColor")
fun setTextColor(view: SegmentedControl, textColor: String?) {
view.setTextColorProp(textColor)
}
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any>? =
MapBuilder.builder<String, Any>()
.put(
"topChange",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onChange", "captured", "onChangeCapture"),
),
)
.build()
}

View File

@ -5,13 +5,13 @@ import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class CustomSegmentedControlPackage : ReactPackage {
class SegmentedControlPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return emptyList()
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return listOf(CustomSegmentedControlManager())
return listOf(SegmentedControlManager())
}
}
}

View File

@ -0,0 +1,6 @@
#import <React/RCTViewManager.h>
@interface RCT_EXTERN_MODULE(SegmentedControlManager, RCTViewManager)
@end

View File

@ -0,0 +1,23 @@
import Foundation
import UIKit
import React
@objc(SegmentedControlManager)
final class SegmentedControlManager: RCTViewManager {
override class func requiresMainQueueSetup() -> Bool { true }
override func view() -> UIView! {
return SegmentedControlView()
}
@objc class func propConfig_values() -> [String]! { ["NSArray"] }
@objc class func propConfig_selectedIndex() -> [String]! { ["NSInteger"] }
@objc class func propConfig_enabled() -> [String]! { ["BOOL"] }
@objc class func propConfig_momentary() -> [String]! { ["BOOL"] }
@objc class func propConfig_tintColor() -> [String]! { ["UIColor"] }
@objc class func propConfig_backgroundColor() -> [String]! { ["UIColor"] }
@objc class func propConfig_textColor() -> [String]! { ["UIColor"] }
@objc class func propConfig_onChange() -> [String]! { ["RCTBubblingEventBlock"] }
}

View File

@ -0,0 +1,98 @@
import UIKit
import React
@objc(SegmentedControlView)
final class SegmentedControlView: UIView {
private let segmentedControl = UISegmentedControl()
// MARK: - Lifecycle
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
segmentedControl.addTarget(self, action: #selector(handleValueChanged(_:)), for: .valueChanged)
addSubview(segmentedControl)
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
segmentedControl.leadingAnchor.constraint(equalTo: leadingAnchor),
segmentedControl.trailingAnchor.constraint(equalTo: trailingAnchor),
segmentedControl.topAnchor.constraint(equalTo: topAnchor),
segmentedControl.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
// MARK: - Prop setters
@objc var values: NSArray = [] {
didSet { rebuildSegments() }
}
@objc var selectedIndex: Int = 0 {
didSet {
guard segmentedControl.numberOfSegments > 0 else { return }
let clamped = min(max(selectedIndex, 0), segmentedControl.numberOfSegments - 1)
if segmentedControl.selectedSegmentIndex != clamped {
segmentedControl.selectedSegmentIndex = clamped
}
}
}
@objc var enabled: Bool = true {
didSet { segmentedControl.isEnabled = enabled }
}
@objc var momentary: Bool = false {
didSet { segmentedControl.isMomentary = momentary }
}
@objc var textColor: UIColor? {
didSet { applyTextAttributes() }
}
@objc var onChange: RCTBubblingEventBlock?
override var tintColor: UIColor! {
didSet { segmentedControl.selectedSegmentTintColor = tintColor }
}
override var backgroundColor: UIColor? {
didSet { segmentedControl.backgroundColor = backgroundColor }
}
// MARK: - Private helpers
private func rebuildSegments() {
let titles = values as? [String] ?? []
segmentedControl.removeAllSegments()
for (i, title) in titles.enumerated() {
segmentedControl.insertSegment(withTitle: title, at: i, animated: false)
}
guard !titles.isEmpty else { return }
let clamped = min(max(selectedIndex, 0), titles.count - 1)
segmentedControl.selectedSegmentIndex = clamped
}
private func applyTextAttributes() {
if let color = textColor {
segmentedControl.setTitleTextAttributes([.foregroundColor: color], for: .normal)
segmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
} else {
segmentedControl.setTitleTextAttributes(nil, for: .normal)
segmentedControl.setTitleTextAttributes(nil, for: .selected)
}
}
@objc private func handleValueChanged(_ sender: UISegmentedControl) {
onChange?(["selectedIndex": sender.selectedSegmentIndex])
}
}

View File

@ -1,7 +1,7 @@
import Bugsnag from '@bugsnag/react-native';
import { getUniqueId } from 'react-native-device-info';
import { BlueApp as BlueAppClass } from '../class';
import { BlueApp as BlueAppClass } from '../class/blue-app';
const BlueApp = BlueAppClass.getInstance();
@ -13,43 +13,41 @@ const BlueApp = BlueAppClass.getInstance();
let userHasOptedOut: boolean = false;
(async () => {
// Don't try to start Bugsnag again as it's already initialized in native code
// Just configure the existing instance if tracking is allowed
const uniqueID = await getUniqueId();
const doNotTrack = await BlueApp.isDoNotTrackEnabled();
try {
// Don't try to start Bugsnag again as it's already initialized in native code.
// Configure it only when tracking is allowed.
const doNotTrack = await BlueApp.isDoNotTrackEnabled();
if (doNotTrack) {
userHasOptedOut = true;
return;
}
if (doNotTrack) {
userHasOptedOut = true;
return;
const uniqueID = await getUniqueId();
Bugsnag.setUser(uniqueID);
Bugsnag.addOnError(function () {
return !userHasOptedOut;
});
} catch (error) {
// Never let analytics setup crash the app.
console.error('Failed to initialize analytics:', error);
}
// Configure the existing Bugsnag instance instead of starting a new one
Bugsnag.setUser(uniqueID);
// Add additional configuration if needed
Bugsnag.addOnError(function (event) {
return !userHasOptedOut;
});
})();
const A = async (event: string) => {};
A.ENUM = {
INIT: 'INIT',
GOT_NONZERO_BALANCE: 'GOT_NONZERO_BALANCE',
GOT_ZERO_BALANCE: 'GOT_ZERO_BALANCE',
CREATED_WALLET: 'CREATED_WALLET',
CREATED_LIGHTNING_WALLET: 'CREATED_LIGHTNING_WALLET',
APP_UNSUSPENDED: 'APP_UNSUSPENDED',
};
A.setOptOut = (value: boolean) => {
if (value) userHasOptedOut = true;
};
A.logError = (errorString: string) => {
console.error(errorString);
Bugsnag.notify(new Error(String(errorString)));
if (!userHasOptedOut) {
try {
Bugsnag.notify(new Error(String(errorString)));
} catch (error) {
console.error('Failed to report error to Bugsnag:', error);
}
}
};
export default A;

View File

@ -0,0 +1,71 @@
// Per-wallet Realm storage for notification-suppression entries.
//
// Lives inside the per-wallet Arkade Realm so suppression state is
// bucket-scoped, encrypted by the wallet's existing Realm key, and removed
// automatically when the wallet is deleted (deleteArkadeRealm tears down the
// whole file). Avoids leaking a stable per-wallet handle into a global
// AsyncStorage key.
export type ArkSwapNotificationAction = 'claim' | 'refund';
// Realm schema. `realm` is a peer dependency we don't import here directly;
// the schema is a plain object consumed by realmInstance.ts via the schemas
// array. Pattern matches BoltzSwapSchema in @arkade-os/boltz-swap.
export const ArkSwapNotificationSuppressionSchema = {
name: 'ArkSwapNotificationSuppression',
primaryKey: 'id',
properties: {
id: 'string',
swapId: 'string',
action: 'string',
postedAt: 'int',
},
};
const compositeId = (swapId: string, action: ArkSwapNotificationAction): string => `${swapId}:${action}`;
interface ArkSwapNotificationSuppressionRow {
id: string;
swapId: string;
action: ArkSwapNotificationAction;
postedAt: number;
}
export class RealmNotificationSuppressionRepository {
private readonly realm: any;
constructor(realm: any) {
this.realm = realm;
}
has(swapId: string, action: ArkSwapNotificationAction): boolean {
const row = this.realm.objectForPrimaryKey('ArkSwapNotificationSuppression', compositeId(swapId, action));
return Boolean(row);
}
record(swapId: string, action: ArkSwapNotificationAction): void {
this.realm.write(() => {
const row: ArkSwapNotificationSuppressionRow = {
id: compositeId(swapId, action),
swapId,
action,
postedAt: Date.now(),
};
this.realm.create('ArkSwapNotificationSuppression', row, 'modified');
});
}
clearForSwap(swapId: string): void {
this.realm.write(() => {
const matches = this.realm.objects('ArkSwapNotificationSuppression').filtered('swapId == $0', swapId);
this.realm.delete(matches);
});
}
clearForSwapAction(swapId: string, action: ArkSwapNotificationAction): void {
this.realm.write(() => {
const row = this.realm.objectForPrimaryKey('ArkSwapNotificationSuppression', compositeId(swapId, action));
if (row) this.realm.delete(row);
});
}
}

View File

@ -0,0 +1,197 @@
import RNFS from 'react-native-fs';
import Realm from 'realm';
import Keychain, { ACCESSIBLE, SECURITY_LEVEL } from 'react-native-keychain';
import { ArkRealmSchemas, ARK_REALM_SCHEMA_VERSION, runArkRealmMigrations } from '@arkade-os/sdk/repositories/realm';
import { BoltzRealmSchemas } from '@arkade-os/boltz-swap/repositories/realm';
import { randomBytes } from '../../../class/rng';
import { uint8ArrayToHex, hexToUint8Array } from '../../uint8array-extras';
import { ArkSwapNotificationSuppressionSchema } from './notificationSuppressionRepository';
const AllArkadeSchemas = [...ArkRealmSchemas, ...BoltzRealmSchemas, ArkSwapNotificationSuppressionSchema];
// App-owned schemas added on top of the SDK's. Bump when an app-owned schema
// changes; SDK bumps are handled by ARK_REALM_SCHEMA_VERSION. Realm requires
// a strictly increasing schemaVersion when objects are added; computing
// `SDK + offset` keeps the local additions ahead of any future SDK bump.
const LOCAL_ARK_SCHEMA_OFFSET = 1;
const ARKADE_REALM_SCHEMA_VERSION = ARK_REALM_SCHEMA_VERSION + LOCAL_ARK_SCHEMA_OFFSET;
const realmInstances: Map<string, Realm> = new Map();
const openInFlight: Map<string, Promise<Realm>> = new Map();
// Files live in a dedicated subdirectory so BlueApp.moveRealmFilesToCacheDirectory()
// — which sweeps top-level *.realm files from Documents into the OS-purgeable cache
// — never sees them. RNFS.readDir is non-recursive, so the subdirectory is invisible
// to that scan. Ark Realm holds non-recoverable swap/claim data and must stay in
// Documents.
const arkadeDir = (): string => `${RNFS.DocumentDirectoryPath}/arkade`;
const realmPathFor = (namespace: string): string => `${arkadeDir()}/arkade-${namespace}.realm`;
const keychainServiceFor = (namespace: string): string => `arkade_realm_${namespace}`;
async function ensureArkadeDir(): Promise<void> {
const dir = arkadeDir();
if (!(await RNFS.exists(dir))) await RNFS.mkdir(dir);
}
async function loadOrCreateEncryptionKey(namespace: string): Promise<Uint8Array> {
const service = keychainServiceFor(namespace);
const credentials = await Keychain.getGenericPassword({ service });
if (credentials) return hexToUint8Array(credentials.password);
const buf = await randomBytes(64);
const password = uint8ArrayToHex(buf);
// Accessibility: match the rest of the app's secret accessibility. RNSecureKeyStore
// in class/blue-app.ts and hooks/useBiometrics.ts both use WHEN_UNLOCKED_THIS_DEVICE_ONLY;
// the default of AFTER_FIRST_UNLOCK would expose the Realm key while the device is locked.
//
// Security level: preflight via getSecurityLevel() rather than try/catch around
// SECURE_HARDWARE. getSecurityLevel returns null on iOS (where the option is moot)
// and the highest supported level on Android. We only opt into SECURE_HARDWARE when
// the device actually backs it; otherwise let react-native-keychain pick its default.
// Catching every setGenericPassword error and silently retrying with ANY (the previous
// shape) downgrades on unrelated failures — preflight surfaces those instead.
const supportedLevel = await Keychain.getSecurityLevel();
const opts: Parameters<typeof Keychain.setGenericPassword>[2] = {
service,
accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
};
if (supportedLevel === SECURITY_LEVEL.SECURE_HARDWARE) {
opts.securityLevel = SECURITY_LEVEL.SECURE_HARDWARE;
}
await Keychain.setGenericPassword(service, password, opts);
return hexToUint8Array(password);
}
/**
* Returns a per-wallet Realm instance keyed by `namespace`. Each Ark wallet
* gets its own encrypted Realm file and its own Keychain entry so wallets
* never collide on WalletState/contracts/swaps and storage buckets stay
* isolated.
*
* Concurrent callers for the same namespace receive the same in-flight
* promise. Errors are surfaced to the caller; the in-flight entry is cleared
* so a later retry can succeed.
*/
export async function getArkadeRealm(namespace: string): Promise<Realm> {
const cached = realmInstances.get(namespace);
if (cached && !cached.isClosed) return cached;
if (cached && cached.isClosed) realmInstances.delete(namespace);
const inFlight = openInFlight.get(namespace);
if (inFlight) return inFlight;
const opening = (async () => {
await ensureArkadeDir();
const encryptionKey = await loadOrCreateEncryptionKey(namespace);
const realm = await Realm.open({
schema: AllArkadeSchemas as unknown as Realm.ObjectSchema[],
schemaVersion: ARKADE_REALM_SCHEMA_VERSION,
onMigration: (oldRealm, newRealm) => {
runArkRealmMigrations(oldRealm, newRealm);
},
path: realmPathFor(namespace),
encryptionKey,
excludeFromIcloudBackup: true,
});
realmInstances.set(namespace, realm);
return realm;
})();
openInFlight.set(namespace, opening);
try {
return await opening;
} finally {
openInFlight.delete(namespace);
}
}
/**
* Close the cached Realm for `namespace`, if any. The file and Keychain
* entry are preserved.
*/
export function closeArkadeRealm(namespace: string): void {
const realm = realmInstances.get(namespace);
if (realm && !realm.isClosed) {
realm.removeAllListeners();
realm.close();
}
realmInstances.delete(namespace);
}
/**
* Close every cached Arkade Realm instance. Used on app shutdown / sign out.
*/
export function closeAllArkadeRealms(): void {
for (const ns of Array.from(realmInstances.keys())) {
closeArkadeRealm(ns);
}
}
/**
* Delete the Realm file and the Keychain entry for `namespace`. Used when
* an Ark wallet is removed. Failures are logged but do not throw leaving
* an orphan file or Keychain entry is preferable to crashing the app's
* delete path. Ark Realm failures stay scoped to the Ark wallet path.
*
* The Keychain encryption key is reset only when the Realm file is gone
* (or never existed). Resetting the key while the encrypted file remains
* would leave the user unable to open the orphan on a future re-import:
* a fresh random key would be generated and the old file's ciphertext
* could not be decrypted.
*/
export async function deleteArkadeRealm(namespace: string): Promise<void> {
closeArkadeRealm(namespace);
const path = realmPathFor(namespace);
let realmRemoved = false;
try {
// Realm.deleteFile is sync and removes the .realm + .lock + .management
// siblings in one call. It is forgiving when the file does not exist
// (no-op), but we guard via Realm.exists to keep behavior explicit.
if (Realm.exists(path)) {
Realm.deleteFile({ path });
}
realmRemoved = true;
} catch (e: any) {
console.log(`[ArkadeRealm] Realm.deleteFile failed for ${path}:`, e?.message ?? e);
}
// Best-effort sweep of any sibling files Realm.deleteFile might have left
// behind. These are not load-bearing for re-import; failures are tolerated.
for (const suffix of ['.note']) {
const sibling = `${path}${suffix}`;
try {
if (await RNFS.exists(sibling)) await RNFS.unlink(sibling);
} catch (e: any) {
console.log(`[ArkadeRealm] failed to delete ${sibling}:`, e?.message ?? e);
}
}
if (!realmRemoved) {
console.log(
`[ArkadeRealm] keeping encryption key for ${namespace} because Realm file cleanup failed; key preserved so a future delete retry can still decrypt the orphan`,
);
return;
}
try {
await Keychain.resetGenericPassword({ service: keychainServiceFor(namespace) });
} catch (e: any) {
console.log(`[ArkadeRealm] failed to reset keychain for ${namespace}:`, e?.message ?? e);
}
}
// Exported for tests only.
export const __testing__ = {
realmInstances,
openInFlight,
realmPathFor,
keychainServiceFor,
};

View File

@ -0,0 +1,423 @@
// Background task module for Ark swap monitoring.
//
// Responsibilities:
// - Passive monitoring: poll Boltz swap status for non-terminal swaps in
// every Ark wallet's per-wallet Realm and persist remote changes through
// the SDK update helpers.
// - Post a local notification when an SDK predicate flags a swap as
// claimable/refundable. No claim, refund, recover, or signing happens in
// background — those remain foreground-only.
//
// State here is in-process: it survives configure→fetch→fetch ticks within a
// single JS runtime but is gone after process kill. Realm remains the
// durable source of truth for swap status and notification suppression.
import BackgroundFetch from 'react-native-background-fetch';
import {
BoltzSwapProvider,
isChainFinalStatus,
isReverseFinalStatus,
isSubmarineFinalStatus,
updateChainSwapStatus,
updateReverseSwapStatus,
updateSubmarineSwapStatus,
} from '@arkade-os/boltz-swap';
import type { BoltzChainSwap, BoltzReverseSwap, BoltzSubmarineSwap, BoltzSwap } from '@arkade-os/boltz-swap';
import { RealmSwapRepository } from '@arkade-os/boltz-swap/repositories/realm';
import { BlueApp as BlueAppClass } from '../class/blue-app';
import { LightningArkWallet } from '../class/wallets/lightning-ark-wallet';
import { getArkadeRealm } from './arkade-adapters/realm/realmInstance';
import {
RealmNotificationSuppressionRepository,
type ArkSwapNotificationAction,
} from './arkade-adapters/realm/notificationSuppressionRepository';
import { notifyArkSwapActionable, resolveActionableAction } from './arkade-notifications';
const BlueApp = BlueAppClass.getInstance();
// Single shared provider. The constructor only stores config; it does not
// open sockets. Re-using one instance avoids per-poll allocation.
const swapProvider = new BoltzSwapProvider({ network: 'bitcoin' });
const DEFAULT_MAX_RUN_MS = 25_000;
let maxRunMs = DEFAULT_MAX_RUN_MS;
interface ArkTaskState {
lastRegisteredAt: number | null;
lastUnregisteredAt: number | null;
lastRunStartedAt: number | null;
lastRunFinishedAt: number | null;
walletsScanned: number;
swapsPolled: number;
swapsUpdated: number;
lastError: string | null;
exitedDueToUnavailableStorage: boolean;
availability: 'unknown' | 'available' | 'denied' | 'restricted';
// Set whenever swapsUpdated is incremented. Used by reconcile() to detect
// updates that crossed run boundaries (per-run swapsUpdated is reset).
lastSwapUpdateAt: number;
lastReconciledAt: number;
}
const state: ArkTaskState = {
lastRegisteredAt: null,
lastUnregisteredAt: null,
lastRunStartedAt: null,
lastRunFinishedAt: null,
walletsScanned: 0,
swapsPolled: 0,
swapsUpdated: 0,
lastError: null,
exitedDueToUnavailableStorage: false,
availability: 'unknown',
lastSwapUpdateAt: 0,
lastReconciledAt: 0,
};
// Per-wallet last-seen status cache. Outer key: wallet namespace; inner key:
// swap ID; value: last status this background module observed. Diagnostic +
// reconciliation hint only — Realm is durable.
const swapStatusCache: Map<string, Map<string, string>> = new Map();
// Per-poll last-seen actionable action keyed by `${namespace}:${swapId}`.
// Used to detect predicate flips (true → false or claim ↔ refund) so we can
// clear the corresponding Realm suppression row even when the swap status
// has not yet reached a terminal state. In-process only; cleared by
// stopArkBackgroundTask so a later run does not falsely diagnose a flip on
// the first poll after restart.
const lastSeenActionMap: Map<string, ArkSwapNotificationAction> = new Map();
let configured = false;
let running = false;
let cancelRequested = false;
let runDeadline: number | null = null;
export function getArkTaskState(): Readonly<ArkTaskState> {
return Object.freeze({ ...state });
}
function recordError(message: string): void {
state.lastError = message;
}
function shouldStopRun(): boolean {
return cancelRequested || (runDeadline !== null && Date.now() >= runDeadline);
}
function remainingRunMs(): number {
if (runDeadline === null) return maxRunMs;
return Math.max(runDeadline - Date.now(), 0);
}
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
promise,
new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => reject(new Error('deadline exceeded')), ms);
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}
function isFinalStatus(swap: BoltzSwap): boolean {
switch (swap.type) {
case 'reverse':
return isReverseFinalStatus(swap.status);
case 'submarine':
return isSubmarineFinalStatus(swap.status);
case 'chain':
return isChainFinalStatus(swap.status);
}
}
async function persistStatusChange(swap: BoltzSwap, newStatus: BoltzSwap['status'], repo: RealmSwapRepository): Promise<void> {
if (swap.type === 'reverse') {
await updateReverseSwapStatus(swap as BoltzReverseSwap, newStatus, s => repo.saveSwap(s));
} else if (swap.type === 'submarine') {
await updateSubmarineSwapStatus(swap as BoltzSubmarineSwap, newStatus, s => repo.saveSwap(s));
} else {
await updateChainSwapStatus(swap as BoltzChainSwap, newStatus, s => repo.saveSwap(s));
}
}
async function pollSwap(
swap: BoltzSwap,
namespace: string,
repo: RealmSwapRepository,
suppression: RealmNotificationSuppressionRepository,
walletID: string,
walletLabel: string,
): Promise<void> {
if (shouldStopRun()) return;
state.swapsPolled += 1;
let response;
try {
response = await withTimeout(swapProvider.getSwapStatus(swap.id), remainingRunMs());
} catch (e: any) {
recordError(`getSwapStatus(${swap.id}): ${e?.message ?? e}`);
if (e?.message === 'deadline exceeded' || remainingRunMs() <= 0) cancelRequested = true;
return;
}
if (shouldStopRun()) return;
const remoteStatus = response.status;
const statusChanged = remoteStatus !== swap.status;
// The SDK update helpers (updateReverseSwapStatus etc.) save a copy and do
// not mutate `swap`, so any post-persist predicate or terminal check on
// `swap` would read the pre-update status. effectiveSwap carries the
// status we want subsequent checks to evaluate against.
const effectiveSwap: BoltzSwap = statusChanged ? ({ ...swap, status: remoteStatus } as BoltzSwap) : swap;
if (statusChanged) {
try {
await persistStatusChange(swap, remoteStatus, repo);
} catch (e: any) {
recordError(`persistStatusChange(${swap.id}): ${e?.message ?? e}`);
return;
}
state.swapsUpdated += 1;
state.lastSwapUpdateAt = Date.now();
let perWallet = swapStatusCache.get(namespace);
if (!perWallet) {
perWallet = new Map();
swapStatusCache.set(namespace, perWallet);
}
perWallet.set(swap.id, remoteStatus);
}
// Actionable evaluation runs on every non-terminal poll, NOT only after a
// status change. Otherwise a swap that became actionable in a previous run
// but never received a successful post (notify failed mid-run, OS-level
// drop, permission-denied skip, app cold-started with already-actionable
// Realm state) would never be re-checked because subsequent polls observe
// remoteStatus === swap.status and would otherwise exit. The Realm
// suppression repo is the dedup layer.
const lastKey = `${namespace}:${effectiveSwap.id}`;
if (isFinalStatus(effectiveSwap)) {
try {
suppression.clearForSwap(effectiveSwap.id);
} catch (e: any) {
recordError(`suppression.clearForSwap(${effectiveSwap.id}): ${e?.message ?? e}`);
}
lastSeenActionMap.delete(lastKey);
return;
}
const action = resolveActionableAction(effectiveSwap);
const lastSeen = lastSeenActionMap.get(lastKey);
if (lastSeen && lastSeen !== action) {
// Predicate flipped out of `lastSeen` (either to null or to the other
// action). Clear the stale suppression so the next observed flip back
// re-fires.
try {
suppression.clearForSwapAction(effectiveSwap.id, lastSeen);
} catch (e: any) {
recordError(`suppression.clearForSwapAction(${effectiveSwap.id}): ${e?.message ?? e}`);
}
}
if (action) {
try {
await notifyArkSwapActionable(effectiveSwap, suppression, walletID, walletLabel);
} catch (e: any) {
recordError(`notifyArkSwapActionable(${effectiveSwap.id}): ${e?.message ?? e}`);
}
lastSeenActionMap.set(lastKey, action);
} else {
lastSeenActionMap.delete(lastKey);
}
}
async function processWallet(wallet: LightningArkWallet): Promise<void> {
state.walletsScanned += 1;
const namespace = wallet.getNamespace();
const walletID = wallet.getID();
const walletLabel = wallet.getLabel();
let realm;
try {
realm = await getArkadeRealm(namespace);
} catch (e: any) {
// Most likely the Keychain is locked (WHEN_UNLOCKED_THIS_DEVICE_ONLY) or
// the Realm file is unreachable. Either way the background task no-ops
// for this wallet — claim/refund is foreground-only anyway.
state.exitedDueToUnavailableStorage = true;
recordError(`getArkadeRealm(${namespace}): ${e?.message ?? e}`);
return;
}
let swaps: BoltzSwap[];
const repo = new RealmSwapRepository(realm as any);
const suppression = new RealmNotificationSuppressionRepository(realm);
try {
swaps = await repo.getAllSwaps<BoltzSwap>();
} catch (e: any) {
recordError(`getAllSwaps(${namespace}): ${e?.message ?? e}`);
return;
}
for (const swap of swaps) {
if (isFinalStatus(swap)) continue;
if (shouldStopRun()) return;
await pollSwap(swap, namespace, repo, suppression, walletID, walletLabel);
}
}
export async function runArkBackgroundTask(taskId: string): Promise<void> {
if (running) {
BackgroundFetch.finish(taskId);
return;
}
running = true;
cancelRequested = false;
runDeadline = Date.now() + maxRunMs;
state.lastRunStartedAt = Date.now();
state.walletsScanned = 0;
state.swapsPolled = 0;
state.swapsUpdated = 0;
state.exitedDueToUnavailableStorage = false;
try {
const wallets = BlueApp.getWallets().filter((w): w is LightningArkWallet => w instanceof LightningArkWallet);
if (wallets.length === 0) return;
for (const wallet of wallets) {
if (shouldStopRun()) break;
try {
await processWallet(wallet);
} catch (e: any) {
recordError(`processWallet: ${e?.message ?? e}`);
}
}
} finally {
state.lastRunFinishedAt = Date.now();
runDeadline = null;
cancelRequested = false;
running = false;
BackgroundFetch.finish(taskId);
}
}
export function onArkBackgroundTaskTimeout(taskId: string): void {
cancelRequested = true;
state.lastError = 'timeout';
state.lastRunFinishedAt = Date.now();
BackgroundFetch.finish(taskId);
}
function availabilityFromStatus(status: number): ArkTaskState['availability'] {
if (status === BackgroundFetch.STATUS_AVAILABLE) return 'available';
if (status === BackgroundFetch.STATUS_DENIED) return 'denied';
if (status === BackgroundFetch.STATUS_RESTRICTED) return 'restricted';
return 'unknown';
}
export async function registerArkBackgroundTask(): Promise<void> {
if (configured) {
await BackgroundFetch.start();
state.lastRegisteredAt = Date.now();
return;
}
const config: Parameters<typeof BackgroundFetch.configure>[0] = {
minimumFetchInterval: 15,
stopOnTerminate: false,
startOnBoot: true,
enableHeadless: true,
requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY,
};
try {
const status = await BackgroundFetch.configure(config, runArkBackgroundTask, onArkBackgroundTaskTimeout);
state.availability = availabilityFromStatus(status);
if (state.availability === 'available') {
configured = true;
state.lastRegisteredAt = Date.now();
} else {
console.warn(`[ArkBackground] Background fetch unavailable: ${state.availability}`);
}
} catch (e: any) {
recordError(`configure: ${e?.message ?? e}`);
}
}
export async function stopArkBackgroundTask(): Promise<void> {
cancelRequested = true;
try {
await BackgroundFetch.stop();
} catch (e: any) {
recordError(`stop: ${e?.message ?? e}`);
}
// Await in-flight run completion (draining). A live background run keeps
// Detox's FabricTimersIdlingResource busy and disconnects the JS bridge.
const start = Date.now();
// eslint-disable-next-line no-unmodified-loop-condition
while (running && Date.now() - start < 30_000) {
await new Promise(resolve => setTimeout(resolve, 50));
}
swapStatusCache.clear();
// Clear in-process predicate-flip tracker so a later run does not
// diagnose a flip on the first poll after restart. Persistent suppression
// (Realm) is intentionally untouched — re-registering must keep history.
lastSeenActionMap.clear();
state.lastUnregisteredAt = Date.now();
}
export function reconcileArkBackgroundTaskResults(triggerRefreshForWallet: (walletId: string) => void): void {
if (state.lastSwapUpdateAt <= state.lastReconciledAt) return;
const wallets = BlueApp.getWallets().filter((w): w is LightningArkWallet => w instanceof LightningArkWallet);
for (const wallet of wallets) {
const namespace = wallet.getNamespace();
const perWallet = swapStatusCache.get(namespace);
if (perWallet && perWallet.size > 0) {
triggerRefreshForWallet(wallet.getID());
}
}
state.lastReconciledAt = Date.now();
}
// Exported for tests only.
export const __testing__ = {
state,
swapStatusCache,
lastSeenActionMap,
resetConfigured: (): void => {
configured = false;
},
setMaxRunMs: (ms: number): void => {
maxRunMs = ms;
},
reset: (): void => {
state.lastRegisteredAt = null;
state.lastUnregisteredAt = null;
state.lastRunStartedAt = null;
state.lastRunFinishedAt = null;
state.walletsScanned = 0;
state.swapsPolled = 0;
state.swapsUpdated = 0;
state.lastError = null;
state.exitedDueToUnavailableStorage = false;
state.availability = 'unknown';
state.lastSwapUpdateAt = 0;
state.lastReconciledAt = 0;
swapStatusCache.clear();
lastSeenActionMap.clear();
configured = false;
running = false;
cancelRequested = false;
runDeadline = null;
maxRunMs = DEFAULT_MAX_RUN_MS;
},
};

View File

@ -0,0 +1,163 @@
// Local-notification posting for actionable Ark swaps. Imported from headless
// background runtimes (no React dependency).
//
// Design notes:
// - Suppression state lives per-wallet in the Arkade Realm
// (RealmNotificationSuppressionRepository), not in a global AsyncStorage
// key — bucket-scoped and encrypted, so the suppression record never
// leaks a stable handle outside the wallet's encryption boundary.
// - Permission and app-level opt-out are checked read-only before each post
// (no prompting from headless context). Suppression is NOT recorded when
// the post is skipped, so a later state where the user grants permission
// triggers a fresh post on the next wake.
// - Notification payload deliberately does NOT include `namespace`. The OS
// notification database persists payloads and is global across BlueWallet
// encryption buckets; embedding a deterministic per-wallet identifier
// would tie a stable handle to the OS-visible record.
import AsyncStorage from '@react-native-async-storage/async-storage';
import { AppState, Platform } from 'react-native';
import { Notification, Notifications } from 'react-native-notifications';
import { checkNotifications, RESULTS } from 'react-native-permissions';
import { isChainSwapClaimable, isChainSwapRefundable, isReverseSwapClaimable, isSubmarineSwapRefundable } from '@arkade-os/boltz-swap';
import type { BoltzSwap } from '@arkade-os/boltz-swap';
import loc from '../loc';
import { NOTIFICATIONS_NO_AND_DONT_ASK_FLAG } from './notifications';
import type {
RealmNotificationSuppressionRepository,
ArkSwapNotificationAction,
} from './arkade-adapters/realm/notificationSuppressionRepository';
export const ARK_SWAP_NOTIFICATION_TYPE = 100;
const ANDROID_NOTIFICATION_CHANNEL_ID = 'channel_01';
let channelEnsured = false;
export function ensureArkNotificationChannel(): void {
if (Platform.OS !== 'android') return;
if (channelEnsured) return;
channelEnsured = true;
// Reuses the BlueWallet channel from blue_modules/notifications.ts:80-91 so
// headless runs do not register a second channel under a different name.
Notifications.setNotificationChannel({
channelId: ANDROID_NOTIFICATION_CHANNEL_ID,
name: 'BlueWallet notifications',
description: 'Notifications about incoming payments',
importance: 4,
enableVibration: true,
showBadge: true,
});
}
// Channel registration runs lazily on the first post (see notifyArkSwapActionable).
// Calling it at module-top would invoke the native bridge during JS bundle
// evaluation, which racy-blocks RN bootstrap on some devices and breaks
// Detox's RN-context wait. The existing blue_modules/notifications.ts pattern
// also defers channel setup to lazy invocation.
export function resolveActionableAction(swap: BoltzSwap): ArkSwapNotificationAction | null {
if (isReverseSwapClaimable(swap) || isChainSwapClaimable(swap)) return 'claim';
if (isSubmarineSwapRefundable(swap) || isChainSwapRefundable(swap)) return 'refund';
return null;
}
const interpolate = (template: string, walletLabel: string): string => template.replace('{walletLabel}', walletLabel);
// Static references so scripts/find-unused-loc.js can detect these keys.
const titleFor = (): string => loc.lndViewInvoice.notification_action_title;
const bodyFor = (action: ArkSwapNotificationAction): string =>
action === 'claim' ? loc.lndViewInvoice.notification_claim_body : loc.lndViewInvoice.notification_refund_body;
let appStateOverrideForTest: string | null = null;
let permissionResultOverrideForTest: string | null = null;
let optOutFlagOverrideForTest: string | null | undefined;
function currentAppState(): string {
return appStateOverrideForTest ?? AppState.currentState;
}
async function isOsNotificationPermissionGranted(): Promise<boolean> {
if (permissionResultOverrideForTest !== null) {
return permissionResultOverrideForTest === RESULTS.GRANTED;
}
try {
const { status } = await checkNotifications();
return status === RESULTS.GRANTED;
} catch {
return false;
}
}
async function isAppLevelOptedOut(): Promise<boolean> {
if (optOutFlagOverrideForTest !== undefined) {
return optOutFlagOverrideForTest === 'true';
}
try {
const flag = await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG);
return flag === 'true';
} catch {
return false;
}
}
export async function notifyArkSwapActionable(
swap: BoltzSwap,
suppression: RealmNotificationSuppressionRepository,
walletID: string,
walletLabel: string,
): Promise<void> {
const action = resolveActionableAction(swap);
if (!action) return;
if (currentAppState() === 'active') return;
if (suppression.has(swap.id, action)) return;
if (!(await isOsNotificationPermissionGranted())) return;
if (await isAppLevelOptedOut()) return;
ensureArkNotificationChannel();
const title = titleFor();
const body = interpolate(bodyFor(action), walletLabel);
try {
Notifications.postLocalNotification(
// namespace is intentionally omitted; tap routing re-derives it from the loaded wallet.
new Notification({
title,
body,
type: ARK_SWAP_NOTIFICATION_TYPE,
walletID,
swapId: swap.id,
action,
}),
);
} catch (e: any) {
console.warn('[ArkNotifications] postLocalNotification failed:', e?.message ?? e);
return;
}
try {
suppression.record(swap.id, action);
} catch (e: any) {
console.warn('[ArkNotifications] suppression.record failed:', e?.message ?? e);
}
}
export const __testing__ = {
resetChannel: (): void => {
channelEnsured = false;
},
setAppStateForTest: (state: string | null): void => {
appStateOverrideForTest = state;
},
setPermissionResultForTest: (result: string | null): void => {
permissionResultOverrideForTest = result;
},
setOptOutFlagForTest: (value: string | null | undefined): void => {
optOutFlagOverrideForTest = value;
},
};

327
blue_modules/bbqr/consts.ts Normal file
View File

@ -0,0 +1,327 @@
/**
* (c) Copyright 2024 by Coinkite Inc. This file is in the public domain.
*
* Constants and fixed values.
*/
import { Version } from './types';
// Fixed-length header
export const HEADER_LEN = 8;
export const FILETYPE_NAMES = {
P: 'PSBT',
T: 'Transaction',
J: 'JSON',
U: 'Unicode Text',
X: 'Executable',
B: 'Binary',
R: 'KT Rx',
S: 'KT Tx',
E: 'KT PSBT',
} as const;
export const ENCODING_NAMES = {
H: 'HEX',
Z: 'Zlib compressed',
'2': 'Base32',
} as const;
export const ENCODINGS = new Set(Object.keys(ENCODING_NAMES));
export const ENCODING_SPLIT_MOD = {
H: 2,
Z: 8,
'2': 8,
} as const;
// taken from: https://github.com/mnooner256/pyqrcode/blob/674a77b5eaf850d063f518bd90c243ee34ad6b5d/pyqrcode/tables.py#L84
export const QR_DATA_CAPACITY = {
1: {
L: { 0: 152, 1: 41, 2: 25, 4: 17, 8: 10 },
M: { 0: 128, 1: 34, 2: 20, 4: 14, 8: 8 },
Q: { 0: 104, 1: 27, 2: 16, 4: 11, 8: 7 },
H: { 0: 72, 1: 17, 2: 10, 4: 7, 8: 4 },
},
2: {
L: { 0: 272, 1: 77, 2: 47, 4: 32, 8: 20 },
M: { 0: 224, 1: 63, 2: 38, 4: 26, 8: 16 },
Q: { 0: 176, 1: 48, 2: 29, 4: 20, 8: 12 },
H: { 0: 128, 1: 34, 2: 20, 4: 14, 8: 8 },
},
3: {
L: { 0: 440, 1: 127, 2: 77, 4: 53, 8: 32 },
M: { 0: 352, 1: 101, 2: 61, 4: 42, 8: 26 },
Q: { 0: 272, 1: 77, 2: 47, 4: 32, 8: 20 },
H: { 0: 208, 1: 58, 2: 35, 4: 24, 8: 15 },
},
4: {
L: { 0: 640, 1: 187, 2: 114, 4: 78, 8: 48 },
M: { 0: 512, 1: 149, 2: 90, 4: 62, 8: 38 },
Q: { 0: 384, 1: 111, 2: 67, 4: 46, 8: 28 },
H: { 0: 288, 1: 82, 2: 50, 4: 34, 8: 21 },
},
5: {
L: { 0: 864, 1: 255, 2: 154, 4: 106, 8: 65 },
M: { 0: 688, 1: 202, 2: 122, 4: 84, 8: 52 },
Q: { 0: 496, 1: 144, 2: 87, 4: 60, 8: 37 },
H: { 0: 368, 1: 106, 2: 64, 4: 44, 8: 27 },
},
6: {
L: { 0: 1088, 1: 322, 2: 195, 4: 134, 8: 82 },
M: { 0: 864, 1: 255, 2: 154, 4: 106, 8: 65 },
Q: { 0: 608, 1: 178, 2: 108, 4: 74, 8: 45 },
H: { 0: 480, 1: 139, 2: 84, 4: 58, 8: 36 },
},
7: {
L: { 0: 1248, 1: 370, 2: 224, 4: 154, 8: 95 },
M: { 0: 992, 1: 293, 2: 178, 4: 122, 8: 75 },
Q: { 0: 704, 1: 207, 2: 125, 4: 86, 8: 53 },
H: { 0: 528, 1: 154, 2: 93, 4: 64, 8: 39 },
},
8: {
L: { 0: 1552, 1: 461, 2: 279, 4: 192, 8: 118 },
M: { 0: 1232, 1: 365, 2: 221, 4: 152, 8: 93 },
Q: { 0: 880, 1: 259, 2: 157, 4: 108, 8: 66 },
H: { 0: 688, 1: 202, 2: 122, 4: 84, 8: 52 },
},
9: {
L: { 0: 1856, 1: 552, 2: 335, 4: 230, 8: 141 },
M: { 0: 1456, 1: 432, 2: 262, 4: 180, 8: 111 },
Q: { 0: 1056, 1: 312, 2: 189, 4: 130, 8: 80 },
H: { 0: 800, 1: 235, 2: 143, 4: 98, 8: 60 },
},
10: {
L: { 0: 2192, 1: 652, 2: 395, 4: 271, 8: 167 },
M: { 0: 1728, 1: 513, 2: 311, 4: 213, 8: 131 },
Q: { 0: 1232, 1: 364, 2: 221, 4: 151, 8: 93 },
H: { 0: 976, 1: 288, 2: 174, 4: 119, 8: 74 },
},
11: {
L: { 0: 2592, 1: 772, 2: 468, 4: 321, 8: 198 },
M: { 0: 2032, 1: 604, 2: 366, 4: 251, 8: 155 },
Q: { 0: 1440, 1: 427, 2: 259, 4: 177, 8: 109 },
H: { 0: 1120, 1: 331, 2: 200, 4: 137, 8: 85 },
},
12: {
L: { 0: 2960, 1: 883, 2: 535, 4: 367, 8: 226 },
M: { 0: 2320, 1: 691, 2: 419, 4: 287, 8: 177 },
Q: { 0: 1648, 1: 489, 2: 296, 4: 203, 8: 125 },
H: { 0: 1264, 1: 374, 2: 227, 4: 155, 8: 96 },
},
13: {
L: { 0: 3424, 1: 1022, 2: 619, 4: 425, 8: 262 },
M: { 0: 2672, 1: 796, 2: 483, 4: 331, 8: 204 },
Q: { 0: 1952, 1: 580, 2: 352, 4: 241, 8: 149 },
H: { 0: 1440, 1: 427, 2: 259, 4: 177, 8: 109 },
},
14: {
L: { 0: 3688, 1: 1101, 2: 667, 4: 458, 8: 282 },
M: { 0: 2920, 1: 871, 2: 528, 4: 362, 8: 223 },
Q: { 0: 2088, 1: 621, 2: 376, 4: 258, 8: 159 },
H: { 0: 1576, 1: 468, 2: 283, 4: 194, 8: 120 },
},
15: {
L: { 0: 4184, 1: 1250, 2: 758, 4: 520, 8: 320 },
M: { 0: 3320, 1: 991, 2: 600, 4: 412, 8: 254 },
Q: { 0: 2360, 1: 703, 2: 426, 4: 292, 8: 180 },
H: { 0: 1784, 1: 530, 2: 321, 4: 220, 8: 136 },
},
16: {
L: { 0: 4712, 1: 1408, 2: 854, 4: 586, 8: 361 },
M: { 0: 3624, 1: 1082, 2: 656, 4: 450, 8: 277 },
Q: { 0: 2600, 1: 775, 2: 470, 4: 322, 8: 198 },
H: { 0: 2024, 1: 602, 2: 365, 4: 250, 8: 154 },
},
17: {
L: { 0: 5176, 1: 1548, 2: 938, 4: 644, 8: 397 },
M: { 0: 4056, 1: 1212, 2: 734, 4: 504, 8: 310 },
Q: { 0: 2936, 1: 876, 2: 531, 4: 364, 8: 224 },
H: { 0: 2264, 1: 674, 2: 408, 4: 280, 8: 173 },
},
18: {
L: { 0: 5768, 1: 1725, 2: 1046, 4: 718, 8: 442 },
M: { 0: 4504, 1: 1346, 2: 816, 4: 560, 8: 345 },
Q: { 0: 3176, 1: 948, 2: 574, 4: 394, 8: 243 },
H: { 0: 2504, 1: 746, 2: 452, 4: 310, 8: 191 },
},
19: {
L: { 0: 6360, 1: 1903, 2: 1153, 4: 792, 8: 488 },
M: { 0: 5016, 1: 1500, 2: 909, 4: 624, 8: 384 },
Q: { 0: 3560, 1: 1063, 2: 644, 4: 442, 8: 272 },
H: { 0: 2728, 1: 813, 2: 493, 4: 338, 8: 208 },
},
20: {
L: { 0: 6888, 1: 2061, 2: 1249, 4: 858, 8: 528 },
M: { 0: 5352, 1: 1600, 2: 970, 4: 666, 8: 410 },
Q: { 0: 3880, 1: 1159, 2: 702, 4: 482, 8: 297 },
H: { 0: 3080, 1: 919, 2: 557, 4: 382, 8: 235 },
},
21: {
L: { 0: 7456, 1: 2232, 2: 1352, 4: 929, 8: 572 },
M: { 0: 5712, 1: 1708, 2: 1035, 4: 711, 8: 438 },
Q: { 0: 4096, 1: 1224, 2: 742, 4: 509, 8: 314 },
H: { 0: 3248, 1: 969, 2: 587, 4: 403, 8: 248 },
},
22: {
L: { 0: 8048, 1: 2409, 2: 1460, 4: 1003, 8: 618 },
M: { 0: 6256, 1: 1872, 2: 1134, 4: 779, 8: 480 },
Q: { 0: 4544, 1: 1358, 2: 823, 4: 565, 8: 348 },
H: { 0: 3536, 1: 1056, 2: 640, 4: 439, 8: 270 },
},
23: {
L: { 0: 8752, 1: 2620, 2: 1588, 4: 1091, 8: 672 },
M: { 0: 6880, 1: 2059, 2: 1248, 4: 857, 8: 528 },
Q: { 0: 4912, 1: 1468, 2: 890, 4: 611, 8: 376 },
H: { 0: 3712, 1: 1108, 2: 672, 4: 461, 8: 284 },
},
24: {
L: { 0: 9392, 1: 2812, 2: 1704, 4: 1171, 8: 721 },
M: { 0: 7312, 1: 2188, 2: 1326, 4: 911, 8: 561 },
Q: { 0: 5312, 1: 1588, 2: 963, 4: 661, 8: 407 },
H: { 0: 4112, 1: 1228, 2: 744, 4: 511, 8: 315 },
},
25: {
L: { 0: 10208, 1: 3057, 2: 1853, 4: 1273, 8: 784 },
M: { 0: 8000, 1: 2395, 2: 1451, 4: 997, 8: 614 },
Q: { 0: 5744, 1: 1718, 2: 1041, 4: 715, 8: 440 },
H: { 0: 4304, 1: 1286, 2: 779, 4: 535, 8: 330 },
},
26: {
L: { 0: 10960, 1: 3283, 2: 1990, 4: 1367, 8: 842 },
M: { 0: 8496, 1: 2544, 2: 1542, 4: 1059, 8: 652 },
Q: { 0: 6032, 1: 1804, 2: 1094, 4: 751, 8: 462 },
H: { 0: 4768, 1: 1425, 2: 864, 4: 593, 8: 365 },
},
27: {
L: { 0: 11744, 1: 3514, 2: 2132, 4: 1465, 8: 902 },
M: { 0: 9024, 1: 2701, 2: 1637, 4: 1125, 8: 692 },
Q: { 0: 6464, 1: 1933, 2: 1172, 4: 805, 8: 496 },
H: { 0: 5024, 1: 1501, 2: 910, 4: 625, 8: 385 },
},
28: {
L: { 0: 12248, 1: 3669, 2: 2223, 4: 1528, 8: 940 },
M: { 0: 9544, 1: 2857, 2: 1732, 4: 1190, 8: 732 },
Q: { 0: 6968, 1: 2085, 2: 1263, 4: 868, 8: 534 },
H: { 0: 5288, 1: 1581, 2: 958, 4: 658, 8: 405 },
},
29: {
L: { 0: 13048, 1: 3909, 2: 2369, 4: 1628, 8: 1002 },
M: { 0: 10136, 1: 3035, 2: 1839, 4: 1264, 8: 778 },
Q: { 0: 7288, 1: 2181, 2: 1322, 4: 908, 8: 559 },
H: { 0: 5608, 1: 1677, 2: 1016, 4: 698, 8: 430 },
},
30: {
L: { 0: 13880, 1: 4158, 2: 2520, 4: 1732, 8: 1066 },
M: { 0: 10984, 1: 3289, 2: 1994, 4: 1370, 8: 843 },
Q: { 0: 7880, 1: 2358, 2: 1429, 4: 982, 8: 604 },
H: { 0: 5960, 1: 1782, 2: 1080, 4: 742, 8: 457 },
},
31: {
L: { 0: 14744, 1: 4417, 2: 2677, 4: 1840, 8: 1132 },
M: { 0: 11640, 1: 3486, 2: 2113, 4: 1452, 8: 894 },
Q: { 0: 8264, 1: 2473, 2: 1499, 4: 1030, 8: 634 },
H: { 0: 6344, 1: 1897, 2: 1150, 4: 790, 8: 486 },
},
32: {
L: { 0: 15640, 1: 4686, 2: 2840, 4: 1952, 8: 1201 },
M: { 0: 12328, 1: 3693, 2: 2238, 4: 1538, 8: 947 },
Q: { 0: 8920, 1: 2670, 2: 1618, 4: 1112, 8: 684 },
H: { 0: 6760, 1: 2022, 2: 1226, 4: 842, 8: 518 },
},
33: {
L: { 0: 16568, 1: 4965, 2: 3009, 4: 2068, 8: 1273 },
M: { 0: 13048, 1: 3909, 2: 2369, 4: 1628, 8: 1002 },
Q: { 0: 9368, 1: 2805, 2: 1700, 4: 1168, 8: 719 },
H: { 0: 7208, 1: 2157, 2: 1307, 4: 898, 8: 553 },
},
34: {
L: { 0: 17528, 1: 5253, 2: 3183, 4: 2188, 8: 1347 },
M: { 0: 13800, 1: 4134, 2: 2506, 4: 1722, 8: 1060 },
Q: { 0: 9848, 1: 2949, 2: 1787, 4: 1228, 8: 756 },
H: { 0: 7688, 1: 2301, 2: 1394, 4: 958, 8: 590 },
},
35: {
L: { 0: 18448, 1: 5529, 2: 3351, 4: 2303, 8: 1417 },
M: { 0: 14496, 1: 4343, 2: 2632, 4: 1809, 8: 1113 },
Q: { 0: 10288, 1: 3081, 2: 1867, 4: 1283, 8: 790 },
H: { 0: 7888, 1: 2361, 2: 1431, 4: 983, 8: 605 },
},
36: {
L: { 0: 19472, 1: 5836, 2: 3537, 4: 2431, 8: 1496 },
M: { 0: 15312, 1: 4588, 2: 2780, 4: 1911, 8: 1176 },
Q: { 0: 10832, 1: 3244, 2: 1966, 4: 1351, 8: 832 },
H: { 0: 8432, 1: 2524, 2: 1530, 4: 1051, 8: 647 },
},
37: {
L: { 0: 20528, 1: 6153, 2: 3729, 4: 2563, 8: 1577 },
M: { 0: 15936, 1: 4775, 2: 2894, 4: 1989, 8: 1224 },
Q: { 0: 11408, 1: 3417, 2: 2071, 4: 1423, 8: 876 },
H: { 0: 8768, 1: 2625, 2: 1591, 4: 1093, 8: 673 },
},
38: {
L: { 0: 21616, 1: 6479, 2: 3927, 4: 2699, 8: 1661 },
M: { 0: 16816, 1: 5039, 2: 3054, 4: 2099, 8: 1292 },
Q: { 0: 12016, 1: 3599, 2: 2181, 4: 1499, 8: 923 },
H: { 0: 9136, 1: 2735, 2: 1658, 4: 1139, 8: 701 },
},
39: {
L: { 0: 22496, 1: 6743, 2: 4087, 4: 2809, 8: 1729 },
M: { 0: 17728, 1: 5313, 2: 3220, 4: 2213, 8: 1362 },
Q: { 0: 12656, 1: 3791, 2: 2298, 4: 1579, 8: 972 },
H: { 0: 9776, 1: 2927, 2: 1774, 4: 1219, 8: 750 },
},
40: {
L: { 0: 23648, 1: 7089, 2: 4296, 4: 2953, 8: 1817 },
M: { 0: 18672, 1: 5596, 2: 3391, 4: 2331, 8: 1435 },
Q: { 0: 13328, 1: 3993, 2: 2420, 4: 1663, 8: 1024 },
H: { 0: 10208, 1: 3057, 2: 1852, 4: 1273, 8: 784 },
},
} as const;
// map version to size in modules
// https://github.com/mnooner256/pyqrcode/blob/674a77b5eaf850d063f518bd90c243ee34ad6b5d/pyqrcode/tables.py#L71
export const QR_SIZE: Record<Version, number> = {
1: 21,
2: 25,
3: 29,
4: 33,
5: 37,
6: 41,
7: 45,
8: 49,
9: 53,
10: 57,
11: 61,
12: 65,
13: 69,
14: 73,
15: 77,
16: 81,
17: 85,
18: 89,
19: 93,
20: 97,
21: 101,
22: 105,
23: 109,
24: 113,
25: 117,
26: 121,
27: 125,
28: 129,
29: 133,
30: 137,
31: 141,
32: 145,
33: 149,
34: 153,
35: 157,
36: 161,
37: 165,
38: 169,
39: 173,
40: 177,
} as const;
// EOF

81
blue_modules/bbqr/join.ts Normal file
View File

@ -0,0 +1,81 @@
/**
* (c) Copyright 2024 by Coinkite Inc. This file is in the public domain.
*
* QR code decoding/joining.
*/
import { ENCODINGS } from './consts';
import { Encoding, JoinResult } from './types';
import { decodeData } from './utils';
/**
* Decodes and joins QR code parts back to binary data.
*
* @param parts Array of QR code parts
* @returns Object containing the file type, encoding, and raw binary data.
*/
export function joinQRs(parts: string[]): JoinResult {
const headers = new Set(parts.map(p => p.slice(0, 6)));
if (headers.size !== 1) {
throw new Error('conflicting/variable filetype/encodings/sizes');
}
const header = [...headers][0];
if (header.slice(0, 2) !== 'B$') {
throw new Error('fixed header not found, expected B$');
}
if (!ENCODINGS.has(header[2])) {
throw new Error(`bad encoding: ${header[2]}`);
}
const encoding = header[2] as Encoding;
const fileType = header[3];
if (!/^[A-Z]$/.test(fileType)) {
throw new Error('fileType must be a single uppercase letter');
}
const numParts = parseInt(header.slice(4, 6), 36);
if (numParts < 1) {
throw new Error('zero parts?');
}
const data = new Map<number, string>();
for (const p of parts) {
const idx = parseInt(p.slice(6, 8), 36);
if (idx >= numParts) {
throw new Error(`got part ${idx} but only expecting ${numParts}`);
}
if (data.has(idx) && data.get(idx) !== p.slice(8)) {
throw new Error(`Duplicate part 0x${idx.toString(16)} has wrong content`);
}
data.set(idx, p.slice(8));
}
const orderedParts = [];
for (let i = 0; i < numParts; i++) {
const p = data.get(i);
if (!p) {
throw new Error(`Part ${i} is missing`);
}
orderedParts.push(p);
}
const raw = decodeData(orderedParts, encoding);
// @ts-ignore
return { fileType, encoding, raw };
}
// EOF

14
blue_modules/bbqr/main.ts Normal file
View File

@ -0,0 +1,14 @@
/**
* (c) Copyright 2024 by Coinkite Inc. This file is in the public domain.
*
* Main entry point for the library.
*/
// import { renderQRImage } from './image.ts';
import { joinQRs } from './join.ts';
import { detectFileType, splitQRs } from './split.ts';
export * from './types';
export { detectFileType, joinQRs, splitQRs };
// EOF

202
blue_modules/bbqr/split.ts Normal file
View File

@ -0,0 +1,202 @@
/**
* (c) Copyright 2024 by Coinkite Inc. This file is in the public domain.
*
* Splitting of data and encoding as BBQr QR codes.
*/
import { ENCODING_SPLIT_MOD, HEADER_LEN } from './consts';
import { Encoding, FileType, SplitOptions, SplitResult, Version } from './types';
import {
base64ToBytes,
encodeData,
fileToBytes,
hexToBytes,
intToBase36,
looksLikePsbt,
validateSplitOptions,
versionToChars,
} from './utils';
function numQRNeeded(version: Version, length: number, encoding: Encoding) {
const splitMod = ENCODING_SPLIT_MOD[encoding];
const baseCap = versionToChars(version) - HEADER_LEN;
// adjust capacity to be a multiple of splitMod
const adjustedCap = baseCap - (baseCap % splitMod);
const estimatedCount = Math.ceil(length / adjustedCap);
if (estimatedCount === 1) {
// if it fits in one QR, we're done
return { count: 1, perEach: length };
}
// the total capacity of our estimated count
// all but the last QR need to use adjusted capacity to ensure proper split
const estimatedCap = (estimatedCount - 1) * adjustedCap + baseCap;
return {
count: estimatedCap >= length ? estimatedCount : estimatedCount + 1,
perEach: adjustedCap,
};
}
function findBestVersion(length: number, opts: Required<SplitOptions>) {
const options: { version: Version; count: number; perEach: number }[] = [];
for (let version = opts.minVersion; version <= opts.maxVersion; version++) {
const { count, perEach } = numQRNeeded(version, length, opts.encoding);
if (opts.minSplit <= count && count <= opts.maxSplit) {
options.push({ version, count, perEach });
}
}
if (!options.length) {
throw new Error('Cannot make it fit');
}
// pick smallest number of QR, lowest version
options.sort((a, b) => a.count - b.count || a.version - b.version);
return options[0];
}
/**
* Converts the input bytes into a series of QR codes, ensuring that the most efficient QR code
* version is used.
*
* NOTE: When the default 'Z' (Zlib) encoding is selected, it is possible that the actual used encoding
* will be '2' (Base32) in case Zlib compression does not reduce the size of the output.
*
* @param raw The input bytes to split and encode.
* @param fileType The file type to use. Refer to BBQr spec.
* @param opts An optional SplitOptions object.
*
* @returns An object containing the version of the QR codes, their string parts, and the actual encoding used.
*/
export function splitQRs(
raw: Uint8Array,
fileType: string,
opts: SplitOptions = {}
): SplitResult {
if (!/^[A-Z]$/.test(fileType)) {
throw new Error('fileType must be a single uppercase letter A-Z');
}
const validatedOpts = validateSplitOptions(opts);
const { encoding: actualEncoding, encoded } = encodeData(raw, validatedOpts.encoding);
const { version, count, perEach } = findBestVersion(encoded.length, validatedOpts);
const parts: string[] = [];
for (let n = 0, offset = 0; offset < encoded.length; n++, offset += perEach) {
parts.push(
`B$${actualEncoding}${fileType}` +
intToBase36(count) +
intToBase36(n) +
encoded.slice(offset, offset + perEach)
);
}
return { version, parts, encoding: actualEncoding };
}
/**
* Takes a given given input (Uint8Array, File, or string) and detects its FileType.
* PSBTs and Bitcoin transactions are supported in raw binary, Base64, or hex format.
*
* @param input - The input to detect the FileType of.
* @returns A Promise that resolves to an object containing the FileType and raw data.
*/
export async function detectFileType(
input: File | Uint8Array | string
): Promise<{ fileType: FileType; raw: Uint8Array }> {
// keep references to both raw and decoded versions of the input to run checks on
let raw: Uint8Array | undefined = undefined;
let decoded: string | undefined = undefined;
if (input instanceof File) {
// convert a File to Uint8Array so we have access to the raw bytes
input = await fileToBytes(input);
}
if (input instanceof Uint8Array) {
// we got binary, see if we recognize it
raw = input;
if (looksLikePsbt(input)) {
console.debug('Detected type "P" from binary input');
return { fileType: 'P', raw };
}
if (raw[0] === 0x01 || raw[0] === 0x02) {
console.debug('Detected type "T" from binary input');
return { fileType: 'T', raw };
}
// otherwise, try to decode as text (could be contents of a file)
try {
decoded = new TextDecoder('utf-8', { fatal: true }).decode(raw);
} catch (err) {
// not text, so fall back to generic binary
console.debug('Detected type "B" from binary input');
return { fileType: 'B', raw };
}
} else if (typeof input === 'string') {
decoded = input;
} else {
throw new Error('Invalid input - must be a File, Uint8Array or string');
}
const trimmed = decoded.trim();
if (/^70736274ff[0-9A-Fa-f]+$/.test(trimmed)) {
// PSBT in hex format
console.debug('Detected type "P" from hex input');
return { fileType: 'P', raw: hexToBytes(trimmed) };
}
if (/^0[1,2]000000[0-9A-Fa-f]+$/.test(trimmed)) {
// Transaction in hex format
console.debug('Detected type "T" from hex input');
return { fileType: 'T', raw: hexToBytes(trimmed) };
}
if (/^[A-Za-z0-9+/=]+$/.test(trimmed)) {
// looks like base64 - could be PSBT or transaction
const bytes = base64ToBytes(decoded);
if (looksLikePsbt(bytes)) {
console.debug('Detected type "P" from base64 input');
return { fileType: 'P', raw: bytes };
}
if (bytes[0] === 0x01 || bytes[0] === 0x02) {
console.debug('Detected type "T" from base64 input');
return { fileType: 'T', raw: bytes };
}
}
// ensure we have raw bytes for the next step
raw = raw ?? new TextEncoder().encode(decoded);
try {
JSON.parse(decoded);
console.debug('Detected type "J"');
return { fileType: 'J', raw };
} catch (err) {
// not JSON - fall back to generic Unicode
console.debug('Detected type "U"');
return { fileType: 'U', raw };
}
}
// EOF

View File

@ -0,0 +1,90 @@
/**
* (c) Copyright 2024 by Coinkite Inc. This file is in the public domain.
*
* Types
*/
import { ENCODING_NAMES, FILETYPE_NAMES, QR_DATA_CAPACITY } from './consts';
export type FileType = keyof typeof FILETYPE_NAMES;
export type Encoding = keyof typeof ENCODING_NAMES;
export type Version = keyof typeof QR_DATA_CAPACITY;
export type SplitOptions = {
/**
* The encoding to use for the split.
* @default 'Z'
*/
encoding?: Encoding;
/**
* The minimum number of QR codes to use.
* @default 1
*/
minSplit?: number;
/**
* The maximum number of QR codes to use.
* @default 1295
*/
maxSplit?: number;
/**
* The minimum version of QR code to use.
* @default 5
*/
minVersion?: Version;
/**
* The maximum version of QR code to use.
* @default 40
*/
maxVersion?: Version;
};
export type SplitResult = {
version: Version;
parts: string[];
encoding: Encoding;
};
export type JoinResult = {
fileType: string;
encoding: Encoding;
raw: Uint8Array;
};
export type ImageOptions = {
/**
* The type of PNG image to render:
*
* - `animated`: An animated PNG (APNG) with a delay between frames.
* - `stacked`: A single PNG image with QR codes stacked vertically.
*
* @default `animated`
*/
mode?: 'animated' | 'stacked';
/**
* The delay between frames in the animated PNG in milliseconds.
* Ignored if `mode` is `stacked`.
* @default 250
*/
frameDelay?: number;
/**
* Whether to randomize the order of the parts.
* Ignored if `mode` is `stacked`.
* @default false.
*/
randomizeOrder?: boolean;
/**
* The scale factor of of the QR code images.
* A scale of 1 means 1 pixel per QR module (black dot).
* @default 4
*/
scale?: number;
/**
* The margin or "quiet zone" around the QR code.
* Numeric values are interpreted as number of modules.
* Percentage values like `10%` are interpreted as a percentage of the QR code size.
* @default 4
*/
margin?: number | `${number}%`;
};
// EOF

212
blue_modules/bbqr/utils.ts Normal file
View File

@ -0,0 +1,212 @@
/**
* (c) Copyright 2024 by Coinkite Inc. This file is in the public domain.
*
* Helper/utility functions.
*/
import { base32 } from '@scure/base';
// @ts-ignore not installing types
import pako from 'pako';
import { QR_DATA_CAPACITY } from './consts';
import type { Encoding, SplitOptions, Version } from './types';
export function hexToBytes(hex: string) {
// convert a hex string to a Uint8Array
const match = hex.match(/.{1,2}/g) ?? [];
return Uint8Array.from(match.map(byte => parseInt(byte, 16)));
}
export function base64ToBytes(base64: string) {
// convert a base64 string to a Uint8Array
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
export function intToBase36(n: number) {
// convert an integer 0-1295 to two digits of base 36 - 00-ZZ
if (n < 0 || n > 1295 || !Number.isInteger(n)) {
throw new Error('Out of range');
}
return n.toString(36).toUpperCase().padStart(2, '0');
}
export async function fileToBytes(file: File) {
// read a File's contents and return as a Uint8Array
const reader = new FileReader();
return new Promise<Uint8Array>((resolve, reject) => {
reader.onload = e => {
const result = e.target?.result;
if (result instanceof ArrayBuffer) {
resolve(new Uint8Array(result));
} else {
reject(new Error('FileReader result is not an ArrayBuffer'));
}
};
reader.readAsArrayBuffer(file);
});
}
function joinByteParts(parts: Uint8Array[]) {
// perf-optimized way to join Uint8Arrays
const length = parts.reduce((acc, bytes) => acc + bytes.length, 0);
const rv = new Uint8Array(length);
let offset = 0;
for (const bytes of parts) {
rv.set(bytes, offset);
offset += bytes.length;
}
return rv;
}
export function isValidVersion(v: number): v is Version {
// act as a TS type guard but also a runtime check
return v in QR_DATA_CAPACITY;
}
export function isValidSplit(s: number) {
return s >= 1 && s <= 1295;
}
export function validateSplitOptions(opts: SplitOptions) {
// ensure all split options are valid, filling in defaults as needed
const allOpts = {
minVersion: opts.minVersion ?? 5,
maxVersion: opts.maxVersion ?? 40,
minSplit: opts.minSplit ?? 1,
maxSplit: opts.maxSplit ?? 1295,
encoding: opts.encoding ?? 'Z',
} as const;
if (allOpts.minVersion > allOpts.maxVersion || !isValidVersion(allOpts.minVersion) || !isValidVersion(allOpts.maxVersion)) {
throw new Error('min/max version out of range');
}
if (!isValidSplit(allOpts.minSplit) || !isValidSplit(allOpts.maxSplit) || allOpts.minSplit > allOpts.maxSplit) {
throw new Error('min/max split out of range');
}
return allOpts;
}
export function looksLikePsbt(data: Uint8Array) {
try {
// 'psbt' + 0xff
return new Uint8Array([0x70, 0x73, 0x62, 0x74, 0xff]).every((b, i) => b === data[i]);
} catch (err) {
return false;
}
}
export function shuffled<T>(arr: T[]): T[] {
// modern Fisher-Yates shuffle (https://en.wikipedia.org/wiki/FisherYates_shuffle#The_modern_algorithm)
// create a copy so we don't mutate the original
arr = [...arr];
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
}
export function versionToChars(v: Version) {
// return number of **chars** that fit into indicated version QR
// - assumes L for ECC
// - assumes alnum encoding
if (!isValidVersion(v)) {
throw new Error('Invalid version');
}
const ecc = 'L';
const encoding = 2; // alnum
return QR_DATA_CAPACITY[v][ecc][encoding];
}
export function encodeData(raw: Uint8Array, encoding?: Encoding) {
// return new encoding (if we upgraded) and the
// characters after encoding (a string)
// - default is Zlib or if compression doesn't help, base32
// - returned data can be split, but must be done modX where X provided
encoding = encoding ?? 'Z';
if (encoding === 'H') {
return {
encoding,
encoded: raw.reduce((acc, byte) => acc + byte.toString(16).padStart(2, '0'), '').toUpperCase(),
};
}
if (encoding === 'Z') {
// trial compression, but skip if it embiggens the data
const compressed = pako.deflate(raw, { windowBits: -10 });
// @ts-ignore wont install types
if (compressed.length >= raw.length) {
encoding = '2';
} else {
encoding = 'Z';
// @ts-ignore wont install types
raw = compressed;
}
}
return {
encoding,
// base32 without padding
encoded: base32.encode(raw).replace(/[=]*$/, ''),
};
}
export function decodeData(parts: string[], encoding: Encoding) {
// decode the parts back into a Uint8Array
if (encoding === 'H') {
return joinByteParts(parts.map(p => hexToBytes(p)));
}
const bytes = joinByteParts(
parts.map(p => {
const padding = (8 - (p.length % 8)) % 8;
return base32.decode(p + '='.repeat(padding));
}),
);
if (encoding === 'Z') {
return pako.inflate(bytes, { windowBits: -10 });
}
return bytes;
}
// EOF

View File

@ -2,4 +2,7 @@
* Let's keep config vars, constants and definitions here
*/
export const groundControlUri: string = 'https://groundcontrol-bluewallet.herokuapp.com';
export const groundControlUri: string = 'https://groundcontrol.bluewallet.io';
/** bitcoin-payment-push-service base URL, no trailing slash. Empty = disabled. */
export const arkadePaymentPushUri: string = 'https://electrum2.bluewallet.io:444';

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 { Platform } from 'react-native';
import { pick, types, keepLocalCopy, errorCodes } from '@react-native-documents/picker';
import RNFS from 'react-native-fs';
import { launchImageLibrary, ImagePickerResponse } from 'react-native-image-picker';
import RNQRGenerator from 'rn-qr-generator';
import { detectQRCodeInImage } from 'react-native-camera-kit-no-google';
import Share from 'react-native-share';
import presentAlert from '../components/Alert';
@ -78,7 +78,7 @@ export const writeFileAndExport = async function (fileName: string, contents: st
export const openSignedTransaction = async function (): Promise<string | false> {
try {
const [res] = await pick({
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', types.json] : [types.allFiles],
type: [types.allFiles],
});
return await _readPsbtFileIntoBase64(res.uri);
@ -113,6 +113,7 @@ export const showImagePickerAndReadImage = async (): Promise<string | undefined>
maxHeight: 800,
maxWidth: 600,
selectionLimit: 1,
includeBase64: true,
});
if (response.didCancel) {
@ -120,19 +121,12 @@ export const showImagePickerAndReadImage = async (): Promise<string | undefined>
} else if (response.errorCode) {
throw new Error(response.errorMessage);
} else if (response.assets) {
try {
const uri = response.assets[0].uri;
if (uri) {
const result = await RNQRGenerator.detect({ uri: decodeURI(uri.toString()) });
if (result?.values.length > 0) {
return result?.values[0];
}
}
throw new Error(loc.send.qr_error_no_qrcode);
} catch (error) {
console.error(error);
presentAlert({ message: loc.send.qr_error_no_qrcode });
const base64 = response.assets[0].base64;
if (base64) {
const result = await detectQRCodeInImage(base64);
if (result) return result;
}
throw new Error(loc.send.qr_error_no_qrcode);
}
return undefined;
@ -145,10 +139,7 @@ export const showImagePickerAndReadImage = async (): Promise<string | undefined>
export const showFilePickerAndReadFile = async function (): Promise<{ data: string | false; uri: string | false }> {
try {
const [pickedFile] = await pick({
type:
Platform.OS === 'ios'
? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', 'io.bluewallet.backup', types.plainText, types.json, types.images]
: [types.allFiles],
type: [types.allFiles],
});
const [localCopy] = await keepLocalCopy({
@ -189,33 +180,23 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri
}
};
const handleImageFile = async (fileCopyUri: string): Promise<{ data: string | false; uri: string | false }> => {
const readFileAsBase64 = async (uri: string): Promise<string> => {
try {
const exists = await RNFS.exists(fileCopyUri);
if (!exists) {
presentAlert({ message: 'File does not exist' });
return { data: false, uri: false };
}
// First attempt: use original URI
let result = await RNQRGenerator.detect({ uri: decodeURI(fileCopyUri) });
if (result?.values && result.values.length > 0) {
return { data: result.values[0], uri: fileCopyUri };
}
// Second attempt: remove file:// prefix and try again
const altUri = fileCopyUri.replace(/^file:\/\//, '');
result = await RNQRGenerator.detect({ uri: decodeURI(altUri) });
if (result?.values && result.values.length > 0) {
return { data: result.values[0], uri: fileCopyUri };
}
presentAlert({ message: loc.send.qr_error_no_qrcode });
return { data: false, uri: false };
} catch (error: any) {
console.error(error);
presentAlert({ message: loc.send.qr_error_no_qrcode });
return { data: false, uri: false };
return await RNFS.readFile(uri, 'base64');
} catch {
return await RNFS.readFile(uri.replace(/^file:\/\//, ''), 'base64');
}
};
const handleImageFile = async (fileCopyUri: string): Promise<{ data: string | false; uri: string | false }> => {
const base64 = await readFileAsBase64(fileCopyUri);
const result = await detectQRCodeInImage(base64);
if (result) {
return { data: result, uri: fileCopyUri };
}
throw new Error(loc.send.qr_error_no_qrcode);
};
export const readFileOutsideSandbox = (filePath: string) => {
if (Platform.OS === 'ios') {
return readFile(filePath);
@ -230,7 +211,7 @@ export const readFileOutsideSandbox = (filePath: string) => {
export const openSignedTransactionRaw: () => Promise<string> = async () => {
try {
const [res] = await pick({
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', types.json] : [types.allFiles],
type: [types.allFiles],
});
const file = await RNFS.readFile(res.uri);
if (file) {
@ -249,7 +230,7 @@ export const openSignedTransactionRaw: () => Promise<string> = async () => {
export const pickTransaction = async () => {
const [res] = await pick({
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', types.plainText, types.json] : [types.allFiles],
type: [types.allFiles],
});
return res;

View File

@ -26,44 +26,93 @@ export interface TinySecp256k1InterfaceExtended {
signDER(h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array;
}
necc.utils.sha256Sync = (...messages: Uint8Array[]): Uint8Array => {
const combinedMessages = messages.reduce((acc, msg) => {
const newArray = new Uint8Array(acc.length + msg.length);
newArray.set(acc);
newArray.set(msg, acc.length);
return newArray;
}, new Uint8Array(0));
return sha256(combinedMessages);
};
// @noble/hashes types differ slightly from @noble/secp256k1 v3 hash slot typings.
necc.hashes.sha256 = sha256 as NonNullable<typeof necc.hashes.sha256>;
necc.hashes.hmacSha256 = ((key: Uint8Array, message: Uint8Array) => hmac(sha256, key, message)) as NonNullable<
typeof necc.hashes.hmacSha256
>;
necc.utils.hmacSha256Sync = (key: Uint8Array, ...messages: Uint8Array[]): Uint8Array => {
const combinedMessages = messages.reduce((acc, msg) => {
const newArray = new Uint8Array(acc.length + msg.length);
newArray.set(acc);
newArray.set(msg, acc.length);
return newArray;
}, new Uint8Array(0));
return hmac(sha256, key, combinedMessages);
};
/* const normal = necc.utils._normalizePrivateKey;
// Removed from @noble/secp256k1 v1.7; vendored from noble test vectors.
// @see https://github.com/paulmillr/noble-secp256k1/blob/1.7.2/test/index.ts
type Hex = string | Uint8Array;
type PrivKey = Hex | bigint | number;
necc.utils.privateAdd = (privateKey: PrivKey, tweak: Hex) => {
console.log({ privateKey, tweak });
const p = normal(privateKey);
const t = normal(tweak);
return necc.utils.privateAdd(necc.utils.mod(p + t, necc.CURVE.n));
}; */
const { mod, secretKeyToScalar, numberToBytesBE, bytesToNumberBE, hexToBytes } = necc.etc;
const CURVE_N = necc.Point.CURVE().n;
function pointFromBytes(p: Uint8Array): necc.Point {
if (p.length === 32) {
const prefixed = new Uint8Array(33);
prefixed[0] = 0x02;
prefixed.set(p, 1);
return necc.Point.fromBytes(prefixed);
}
return necc.Point.fromBytes(p);
}
const tweakUtils = {
privateAdd: (privateKey: Hex, tweak: Hex): Uint8Array => {
const p = secretKeyToScalar(typeof privateKey === 'string' ? hexToBytes(privateKey) : privateKey);
const t = secretKeyToScalar(typeof tweak === 'string' ? hexToBytes(tweak) : tweak);
return numberToBytesBE(mod(p + t, CURVE_N));
},
privateNegate: (privateKey: Hex): Uint8Array => {
const p = secretKeyToScalar(typeof privateKey === 'string' ? hexToBytes(privateKey) : privateKey);
return numberToBytesBE(CURVE_N - p);
},
pointAddScalar: (p: Hex, tweak: Hex, isCompressed?: boolean): Uint8Array => {
const P = typeof p === 'string' ? necc.Point.fromHex(p) : pointFromBytes(p);
const t = secretKeyToScalar(typeof tweak === 'string' ? hexToBytes(tweak) : tweak);
const Q = P.add(necc.Point.BASE.multiply(t));
if (Q.is0()) throw new Error('Tweaked point at infinity');
return Q.toBytes(isCompressed);
},
pointMultiply: (p: Hex, tweak: Hex, isCompressed?: boolean): Uint8Array => {
const P = typeof p === 'string' ? necc.Point.fromHex(p) : pointFromBytes(p);
const tweakBytes = typeof tweak === 'string' ? hexToBytes(tweak) : tweak;
const t = mod(bytesToNumberBE(tweakBytes), CURVE_N);
if (t === 0n) throw new Error('Point at infinity');
return P.multiply(t).toBytes(isCompressed);
},
};
const defaultTrue = (param?: boolean): boolean => param !== false;
function compactToDER(sig: Uint8Array): Uint8Array {
const encodeInt = (bytes: Uint8Array): Uint8Array => {
let i = 0;
while (i < bytes.length - 1 && bytes[i] === 0) i++;
let trimmed = bytes.subarray(i);
if (trimmed[0] >= 0x80) {
const prefixed = new Uint8Array(trimmed.length + 1);
prefixed[0] = 0;
prefixed.set(trimmed, 1);
trimmed = prefixed;
}
const encoded = new Uint8Array(2 + trimmed.length);
encoded[0] = 0x02;
encoded[1] = trimmed.length;
encoded.set(trimmed, 2);
return encoded;
};
const rDer = encodeInt(sig.subarray(0, 32));
const sDer = encodeInt(sig.subarray(32, 64));
const seqLen = rDer.length + sDer.length;
const der = new Uint8Array(2 + seqLen);
der[0] = 0x30;
der[1] = seqLen;
der.set(rDer, 2);
der.set(sDer, 2 + rDer.length);
return der;
}
function throwToNull<Type>(fn: () => Type): Type | null {
try {
return fn();
} catch (e) {
// console.log(e);
return null;
}
}
@ -71,7 +120,8 @@ function throwToNull<Type>(fn: () => Type): Type | null {
function isPoint(p: Uint8Array, xOnly: boolean): boolean {
if ((p.length === 32) !== xOnly) return false;
try {
return !!necc.Point.fromHex(p);
pointFromBytes(p);
return true;
} catch (e) {
return false;
}
@ -79,23 +129,12 @@ function isPoint(p: Uint8Array, xOnly: boolean): boolean {
const ecc: TinySecp256k1InterfaceExtended & TinySecp256k1Interface & TinySecp256k1InterfaceBIP32 = {
isPoint: (p: Uint8Array): boolean => isPoint(p, false),
isPrivate: (d: Uint8Array): boolean => {
/* if (
[
'0000000000000000000000000000000000000000000000000000000000000000',
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142',
].includes(d.toString('hex'))
) {
return false;
} */
return necc.utils.isValidPrivateKey(d);
},
isPrivate: (d: Uint8Array): boolean => necc.utils.isValidSecretKey(d),
isXOnlyPoint: (p: Uint8Array): boolean => isPoint(p, true),
xOnlyPointAddTweak: (p: Uint8Array, tweak: Uint8Array): { parity: 0 | 1; xOnlyPubkey: Uint8Array } | null =>
throwToNull(() => {
const P = necc.utils.pointAddScalar(p, tweak, true);
const P = tweakUtils.pointAddScalar(p, tweak, true);
const parity = P[0] % 2 === 1 ? 1 : 0;
return { parity, xOnlyPubkey: P.slice(1) };
}),
@ -104,60 +143,56 @@ const ecc: TinySecp256k1InterfaceExtended & TinySecp256k1Interface & TinySecp256
throwToNull(() => necc.getPublicKey(sk, defaultTrue(compressed))),
pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array => {
return necc.Point.fromHex(p).toRawBytes(defaultTrue(compressed));
return pointFromBytes(p).toBytes(defaultTrue(compressed));
},
pointMultiply: (a: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null =>
throwToNull(() => necc.utils.pointMultiply(a, tweak, defaultTrue(compressed))),
throwToNull(() => tweakUtils.pointMultiply(a, tweak, defaultTrue(compressed))),
pointAdd: (a: Uint8Array, b: Uint8Array, compressed?: boolean): Uint8Array | null =>
throwToNull(() => {
const A = necc.Point.fromHex(a);
const B = necc.Point.fromHex(b);
return A.add(B).toRawBytes(defaultTrue(compressed));
const A = pointFromBytes(a);
const B = pointFromBytes(b);
return A.add(B).toBytes(defaultTrue(compressed));
}),
pointAddScalar: (p: Uint8Array, tweak: Uint8Array, compressed?: boolean): Uint8Array | null =>
throwToNull(() => necc.utils.pointAddScalar(p, tweak, defaultTrue(compressed))),
throwToNull(() => tweakUtils.pointAddScalar(p, tweak, defaultTrue(compressed))),
privateAdd: (d: Uint8Array, tweak: Uint8Array): Uint8Array | null =>
throwToNull(() => {
// console.log({ d, tweak });
if (d.join('') === '00000000000000000000000000000001' && tweak.join('') === '00000000000000000000000000000000') {
return new Uint8Array(d); // make test_ecc happy
}
const ret = necc.utils.privateAdd(d, tweak);
// console.log(ret);
const ret = tweakUtils.privateAdd(d, tweak);
if (ret.join('') === '00000000000000000000000000000000') {
return null;
}
return ret;
}),
privateNegate: (d: Uint8Array): Uint8Array => necc.utils.privateNegate(d),
privateNegate: (d: Uint8Array): Uint8Array => tweakUtils.privateNegate(d),
sign: (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array => {
return necc.signSync(h, d, { der: false, extraEntropy: e });
return necc.sign(h, d, { prehash: false, extraEntropy: e });
},
signDER: (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array => {
return necc.signSync(h, d, { der: true, extraEntropy: e });
return compactToDER(necc.sign(h, d, { prehash: false, extraEntropy: e }));
},
signSchnorr: (h: Uint8Array, d: Uint8Array, e: Uint8Array = new Uint8Array(32).fill(0x00)): Uint8Array => {
return necc.schnorr.signSync(h, d, e);
return necc.schnorr.sign(h, d, e);
},
verify: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array, strict?: boolean): boolean => {
return necc.verify(signature, h, Q, { strict });
return necc.verify(signature, h, Q, { prehash: false, lowS: strict !== false });
},
verifySchnorr: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean => {
return necc.schnorr.verifySync(signature, h, Q);
return necc.schnorr.verify(signature, h, Q);
},
};
export default ecc;
// module.exports.ecc = ecc;

View File

@ -1,47 +1,176 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import PushNotificationIOS from '@react-native-community/push-notification-ios';
import { AppState, AppStateStatus, Platform } from 'react-native';
import { AppState, AppStateStatus, EmitterSubscription, Platform } from 'react-native';
import { getApplicationName, getSystemName, getSystemVersion, getVersion, hasGmsSync, hasHmsSync } from 'react-native-device-info';
import {
Notification as RNNotification,
NotificationBackgroundFetchResult,
NotificationCompletion,
Notifications,
} from 'react-native-notifications';
import { checkNotifications, requestNotifications, RESULTS } from 'react-native-permissions';
import PushNotification, { ReceivedNotification } from 'react-native-push-notification';
import type { BoltzReverseSwap } from '@arkade-os/boltz-swap';
import loc from '../loc';
import { groundControlUri } from './constants';
import { arkadePaymentPushUri, groundControlUri } from './constants';
import { fetch } from '../util/fetch';
const PUSH_TOKEN = 'PUSH_TOKEN';
const GROUNDCONTROL_BASE_URI = 'GROUNDCONTROL_BASE_URI';
const NOTIFICATIONS_STORAGE = 'NOTIFICATIONS_STORAGE';
const ANDROID_NOTIFICATION_CHANNEL_ID = 'channel_01';
export const NOTIFICATIONS_NO_AND_DONT_ASK_FLAG = 'NOTIFICATIONS_NO_AND_DONT_ASK_FLAG';
let alreadyConfigured = false;
let baseURI = groundControlUri;
const baseURI = groundControlUri;
let notificationSubscriptions: EmitterSubscription[] = [];
let onProcessNotificationsHandler: undefined | (() => void | Promise<void>);
const handledNotificationKeys = new Set<string>();
let pendingRegistrationPromise: Promise<boolean> | null = null;
let pendingRegistrationResolve: ((value: boolean) => void) | null = null;
let pendingRegistrationTimeout: ReturnType<typeof setTimeout> | undefined;
type TPushToken = {
token: string;
os: string; // its actually ('ios' | 'android'), but types for the lib are a bit more generic...
os: 'ios' | 'android';
};
// thats unwrapped `ReceivedNotification`, withall `data` fields inline
type TPayload = {
// inherited from `ReceivedNotification`:
subText?: string;
title?: string;
identifier?: string;
message?: string | object;
foreground: boolean;
userInteraction: boolean;
// hopefully stuffed in `data` and uwrapped when received:
address: string;
txid: string;
type: number;
hash: string;
[key: string]: any;
};
function deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
const createPushToken = (deviceToken: string): TPushToken => ({
token: deviceToken,
os: Platform.OS as TPushToken['os'],
});
const settlePendingRegistration = (value: boolean) => {
if (!pendingRegistrationResolve) return;
const resolve = pendingRegistrationResolve;
pendingRegistrationResolve = null;
pendingRegistrationPromise = null;
if (pendingRegistrationTimeout) {
clearTimeout(pendingRegistrationTimeout);
pendingRegistrationTimeout = undefined;
}
resolve(value);
};
const waitForRemoteRegistration = (timeoutMs = 10_000): Promise<boolean> => {
if (pendingRegistrationPromise) return pendingRegistrationPromise;
pendingRegistrationPromise = new Promise<boolean>(resolve => {
pendingRegistrationResolve = resolve;
pendingRegistrationTimeout = setTimeout(() => {
settlePendingRegistration(false);
}, timeoutMs);
});
Notifications.registerRemoteNotifications();
return pendingRegistrationPromise;
};
const ensureAndroidNotificationChannel = () => {
if (Platform.OS !== 'android') return;
Notifications.setNotificationChannel({
channelId: ANDROID_NOTIFICATION_CHANNEL_ID,
name: 'BlueWallet notifications',
description: 'Notifications about incoming payments',
importance: 4,
enableVibration: true,
showBadge: true,
});
};
const getNotificationKey = (payload: Partial<TPayload>, notification?: RNNotification) => {
return JSON.stringify({
identifier: notification?.identifier ?? payload.identifier ?? '',
type: payload.type ?? '',
hash: payload.hash ?? '',
txid: payload.txid ?? '',
address: payload.address ?? '',
message: payload.message ?? '',
});
};
const markNotificationHandled = (key: string) => {
handledNotificationKeys.add(key);
if (handledNotificationKeys.size > 100) {
const oldestKey = handledNotificationKeys.values().next().value;
if (oldestKey) handledNotificationKeys.delete(oldestKey);
}
};
const normalizeNotificationPayload = (notification: RNNotification, status: Pick<TPayload, 'foreground' | 'userInteraction'>): TPayload => {
const rawPayload =
notification.payload && typeof notification.payload === 'object' ? (deepClone(notification.payload) as Record<string, any>) : {};
const nestedPayload = rawPayload.data && typeof rawPayload.data === 'object' ? rawPayload.data : {};
const nestedData = nestedPayload.data && typeof nestedPayload.data === 'object' ? nestedPayload.data : {};
const payload: TPayload = {
...rawPayload,
...nestedPayload,
...nestedData,
title: notification.title ?? rawPayload.title,
subText: rawPayload.subText ?? rawPayload.subtitle ?? notification.title,
message: rawPayload.message ?? notification.body,
identifier: notification.identifier,
foreground: status.foreground,
userInteraction: status.userInteraction,
} as TPayload;
delete payload.data;
return payload;
};
const storeIncomingNotification = async (
notification: RNNotification,
status: Pick<TPayload, 'foreground' | 'userInteraction'>,
completion?: ((response: NotificationCompletion) => void) | ((response: NotificationBackgroundFetchResult) => void),
) => {
try {
const payload = normalizeNotificationPayload(notification, status);
const notificationKey = getNotificationKey(payload, notification);
if (handledNotificationKeys.has(notificationKey)) {
return;
}
markNotificationHandled(notificationKey);
if (!payload.subText && !payload.message) {
console.warn('Notification missing required fields:', payload);
return;
}
await addNotification(payload);
if (payload.foreground && onProcessNotificationsHandler) {
await onProcessNotificationsHandler();
}
} catch (error) {
console.error('Failed to store incoming notification:', error);
} finally {
if (completion) {
if (status.foreground) {
(completion as (response: NotificationCompletion) => void)({ alert: false, sound: false, badge: false });
} else {
(completion as (response: NotificationBackgroundFetchResult) => void)(NotificationBackgroundFetchResult.NO_DATA);
}
}
}
};
const checkAndroidNotificationPermission = async () => {
try {
const { status } = await checkNotifications();
console.debug('Notification permission check:', status);
console.log('Notification permission check:', status);
return status === RESULTS.GRANTED;
} catch (err) {
console.error('Failed to check notification permission:', err);
@ -62,17 +191,21 @@ export const checkNotificationPermissionStatus = async () => {
// Listener to monitor notification permission status changes while app is running
let currentPermissionStatus = 'unavailable';
const handleAppStateChange = async (nextAppState: AppStateStatus) => {
if (nextAppState === 'active') {
const isDisabledByUser = (await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG)) === 'true';
if (!isDisabledByUser) {
const newPermissionStatus = await checkNotificationPermissionStatus();
if (newPermissionStatus !== currentPermissionStatus) {
currentPermissionStatus = newPermissionStatus;
if (newPermissionStatus === 'granted') {
await initializeNotifications();
try {
if (nextAppState === 'active') {
const isDisabledByUser = (await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG)) === 'true';
if (!isDisabledByUser) {
const newPermissionStatus = await checkNotificationPermissionStatus();
if (newPermissionStatus !== currentPermissionStatus) {
currentPermissionStatus = newPermissionStatus;
if (newPermissionStatus === 'granted') {
await initializeNotifications();
}
}
}
}
} catch (error) {
console.error('Failed handling app state notification refresh:', error);
}
};
@ -86,22 +219,14 @@ export const cleanUserOptOutFlag = async () => {
* Should be called when user is most interested in receiving push notifications.
* If we dont have a token it will show alert asking whether
* user wants to receive notifications, and if yes - will configure push notifications.
* FYI, on Android permissions are acquired when app is installed, so basically we dont need to ask,
* we can just call `configure`. On iOS its different, and calling `configure` triggers system's dialog box.
*
* @returns {Promise<boolean>} TRUE if permissions were obtained, FALSE otherwise
*/
/**
* Attempts to obtain permissions and configure notifications.
* Shows a rationale on Android if permissions are needed.
*
* @returns {Promise<boolean>}
*/
export const tryToObtainPermissions = async () => {
console.debug('tryToObtainPermissions: Starting user-triggered permission request');
export const tryToObtainPermissions = async (): Promise<boolean> => {
console.log('tryToObtainPermissions: Starting user-triggered permission request');
if (!isNotificationsCapable) {
console.debug('tryToObtainPermissions: Device not capable');
console.log('tryToObtainPermissions: Device not capable');
return false;
}
@ -118,7 +243,7 @@ export const tryToObtainPermissions = async () => {
Platform.OS === 'android' && Platform.Version < 33 ? rationale : undefined,
);
if (status !== RESULTS.GRANTED) {
console.debug('tryToObtainPermissions: Permission denied');
console.log('tryToObtainPermissions: Permission denied');
return false;
}
return configureNotifications();
@ -127,6 +252,29 @@ export const tryToObtainPermissions = async () => {
return false;
}
};
export const enqueueTestPushNotification = async (): Promise<void> => {
const pushToken = await getPushToken();
if (!pushToken?.token || !pushToken?.os) {
throw new Error('No push token available');
}
const response = await fetch(`${baseURI}/enqueue`, {
method: 'POST',
headers: _getHeaders(),
body: JSON.stringify({
type: 5,
token: pushToken.token,
os: pushToken.os,
text: 'Test push notification',
}),
});
if (!response.ok) {
throw new Error(`Enqueue request failed with status ${response.status}: ${response.statusText}`);
}
};
/**
* Submits onchain bitcoin addresses and ln invoice preimage hashes to GroundControl server, so later we could
* be notified if they were paid
@ -137,7 +285,7 @@ export const tryToObtainPermissions = async () => {
* @returns {Promise<object>} Response object from API rest call
*/
export const majorTomToGroundControl = async (addresses: string[], hashes: string[], txids: string[]) => {
console.debug('majorTomToGroundControl: Starting notification registration', {
console.log('majorTomToGroundControl: Starting notification registration', {
addressCount: addresses?.length,
hashCount: hashes?.length,
txidCount: txids?.length,
@ -155,7 +303,7 @@ export const majorTomToGroundControl = async (addresses: string[], hashes: strin
}
const pushToken = await getPushToken();
console.debug('majorTomToGroundControl: Retrieved push token:', !!pushToken);
console.log('majorTomToGroundControl: Retrieved push token:', !!pushToken);
if (!pushToken || !pushToken.token || !pushToken.os) {
return;
}
@ -170,7 +318,7 @@ export const majorTomToGroundControl = async (addresses: string[], hashes: strin
let response;
try {
console.debug('majorTomToGroundControl: Sending request to:', `${baseURI}/majorTomToGroundControl`);
console.log('majorTomToGroundControl: Sending request to:', `${baseURI}/majorTomToGroundControl`);
response = await fetch(`${baseURI}/majorTomToGroundControl`, {
method: 'POST',
headers: _getHeaders(),
@ -202,6 +350,44 @@ export const majorTomToGroundControl = async (addresses: string[], hashes: strin
}
};
/**
* Registers an Ark swap with the bitcoin-payment-push-service so the device is
* pushed when the invoice gets paid. Fire-and-forget: never throws, gated by
* the same opt-out/token rules as majorTomToGroundControl(). The swap's
* preimage is always stripped before leaving the device.
*/
export const registerArkPaymentPush = async (paymentHash: string, label: string, pendingSwap: BoltzReverseSwap): Promise<void> => {
if (!arkadePaymentPushUri) return;
try {
const noAndDontAskFlag = await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG);
if (noAndDontAskFlag === 'true') {
console.warn('User has opted out of notifications.');
return;
}
const pushToken = await getPushToken();
if (!pushToken || !pushToken.token || !pushToken.os) {
return;
}
const response = await fetch(`${arkadePaymentPushUri}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
topic: paymentHash,
label,
swap: { ...pendingSwap, preimage: '' },
}),
});
if (!response.ok) {
throw new Error(`status ${response.status}`);
}
console.log('[ARK] payment push registration ok');
} catch (e: any) {
console.log('[ARK] payment push registration failed:', e?.message ?? e);
}
};
/**
* Returns a permissions object:
* alert: boolean
@ -212,11 +398,18 @@ export const majorTomToGroundControl = async (addresses: string[], hashes: strin
*/
export const checkPermissions = async () => {
try {
return new Promise(function (resolve) {
PushNotification.checkPermissions((result: any) => {
resolve(result);
});
});
if (Platform.OS === 'ios') {
return Notifications.ios.checkPermissions();
}
const { status } = await checkNotifications();
const granted = status === RESULTS.GRANTED;
return {
alert: granted,
badge: granted,
sound: granted,
status,
};
} catch (error) {
console.error('Error checking permissions:', error);
throw error;
@ -251,12 +444,14 @@ export const setLevels = async (levelAll: boolean) => {
}
if (!levelAll) {
console.debug('Disabling notifications as user opted out...');
PushNotification.removeAllDeliveredNotifications();
PushNotification.setApplicationIconBadgeNumber(0);
PushNotification.cancelAllLocalNotifications();
console.log('Disabling notifications as user opted out...');
Notifications.removeAllDeliveredNotifications();
if (Platform.OS === 'ios') {
Notifications.ios.setBadgeCount(0);
Notifications.ios.cancelAllLocalNotifications();
}
await AsyncStorage.setItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG, 'true');
console.debug('Notifications disabled successfully');
console.log('Notifications disabled successfully');
} else {
await AsyncStorage.removeItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG); // Clear flag when enabling
}
@ -282,19 +477,19 @@ export const addNotification = async (notification: TPayload) => {
};
const postTokenConfig = async () => {
console.debug('postTokenConfig: Starting token configuration');
console.log('postTokenConfig: Starting token configuration');
const pushToken = await getPushToken();
console.debug('postTokenConfig: Retrieved push token:', !!pushToken);
console.log('postTokenConfig: Retrieved push token:', !!pushToken);
if (!pushToken || !pushToken.token || !pushToken.os) {
console.debug('postTokenConfig: Invalid token or missing OS info');
console.log('postTokenConfig: Invalid token or missing OS info');
return;
}
try {
const lang = (await AsyncStorage.getItem('lang')) || 'en';
const appVersion = getSystemName() + ' ' + getSystemVersion() + ';' + getApplicationName() + ' ' + getVersion();
console.debug('postTokenConfig: Posting configuration', { lang, appVersion });
console.log('postTokenConfig: Posting configuration', { lang, appVersion });
await fetch(`${baseURI}/setTokenConfiguration`, {
method: 'POST',
@ -325,101 +520,72 @@ const _setPushToken = async (token: TPushToken) => {
/**
* Configures notifications. For Android, it will show a native rationale prompt if necessary.
*
* @returns {Promise<boolean>}
* @returns {Promise<boolean>} whether successfully registered for remote push notifications
*/
export const configureNotifications = async (onProcessNotifications?: () => void) => {
if (alreadyConfigured) {
console.debug('configureNotifications: Already configured, skipping');
return true;
const configureNotifications = async (onProcessNotifications?: () => void): Promise<boolean> => {
console.log('configureNotifications()');
if (onProcessNotifications) {
onProcessNotificationsHandler = onProcessNotifications;
}
return new Promise(resolve => {
const handleRegistration = async (token: TPushToken) => {
if (__DEV__) {
console.debug('configureNotifications: Token received:', token);
}
alreadyConfigured = true;
await _setPushToken(token);
resolve(true);
};
// const handleNotification = async (notification: TPushNotification & { data: any }) => {
const handleNotification = async (notification: Omit<ReceivedNotification, 'userInfo'>) => {
// Deep clone to avoid modifying the original object
// @ts-ignore some missing properties hopefully will be unwrapped from `.data`
const payload: TPayload = deepClone({
...notification,
...notification.data,
});
if (notification.data?.data) {
const validData = Object.fromEntries(Object.entries(notification.data.data).filter(([_, value]) => value != null));
Object.assign(payload, validData);
}
// @ts-ignore stfu ts, its cleanup
payload.data = undefined;
if (!payload.subText && !payload.message) {
console.warn('Notification missing required fields:', payload);
return;
}
await addNotification(payload);
notification.finish(PushNotificationIOS.FetchResult.NoData);
if (payload.foreground && onProcessNotifications) {
await onProcessNotifications();
}
};
const configure = async () => {
try {
const { status } = await checkNotifications();
if (status !== RESULTS.GRANTED) {
console.debug('configureNotifications: Permissions not granted');
return resolve(false);
}
const existingToken = await getPushToken();
if (existingToken) {
alreadyConfigured = true;
console.debug('Notifications already configured with existing token');
return resolve(true);
}
PushNotification.configure({
onRegister: handleRegistration,
onNotification: handleNotification,
onRegistrationError: (error: any) => {
console.error('Registration error:', error);
resolve(false);
},
permissions: { alert: true, badge: true, sound: true },
popInitialNotification: true,
});
} catch (error) {
console.error('Error in configure:', error);
resolve(false);
}
};
configure();
});
};
/**
* Validates whether the provided GroundControl URI is valid by pinging it.
*
* @param uri {string}
* @returns {Promise<boolean>} TRUE if valid, FALSE otherwise
*/
export const isGroundControlUriValid = async (uri: string) => {
try {
const response = await fetch(`${uri}/ping`, { headers: _getHeaders() });
const json = await response.json();
return !!json.description;
} catch (_) {
const { status } = await checkNotifications();
if (status !== RESULTS.GRANTED) {
console.log('configureNotifications: Permissions not granted');
return false;
}
ensureAndroidNotificationChannel();
if (notificationSubscriptions.length === 0) {
notificationSubscriptions = [
Notifications.events().registerRemoteNotificationsRegistered(async event => {
console.log('processing event', event);
const token = createPushToken(event.deviceToken);
if (__DEV__) {
console.log('configureNotifications: Token received:', token);
}
await _setPushToken(token);
await postTokenConfig().catch(error => console.error('Failed to post token configuration:', error));
settlePendingRegistration(true);
}),
Notifications.events().registerRemoteNotificationsRegistrationFailed(error => {
console.error('Registration error:', error);
settlePendingRegistration(false);
}),
Notifications.events().registerRemoteNotificationsRegistrationDenied(() => {
console.log('Remote notification registration denied');
settlePendingRegistration(false);
}),
Notifications.events().registerNotificationReceivedForeground(async (notification, completion) => {
await storeIncomingNotification(notification, { foreground: true, userInteraction: false }, completion);
}),
Notifications.events().registerNotificationReceivedBackground(async (notification, completion) => {
await storeIncomingNotification(notification, { foreground: false, userInteraction: false }, completion);
}),
Notifications.events().registerNotificationOpened(async (notification, completion) => {
try {
await storeIncomingNotification(notification, { foreground: false, userInteraction: true });
} finally {
completion();
}
}),
];
}
Notifications.getInitialNotification()
.then(async initialNotification => {
if (initialNotification) {
console.log('App was launched by a push notification:', initialNotification);
await storeIncomingNotification(initialNotification, { foreground: false, userInteraction: true });
}
})
.catch(error => console.error('Failed to retrieve initial notification:', error));
// waiting and returning actual result of remote pushes registration: success or failure
return await waitForRemoteRegistration();
} catch (error) {
console.error('Error in configureNotifications:', error);
return false;
}
};
@ -524,9 +690,15 @@ export const clearStoredNotifications = async () => {
export const getDeliveredNotifications: () => Promise<Record<string, any>[]> = () => {
try {
return new Promise(resolve => {
PushNotification.getDeliveredNotifications((notifications: Record<string, any>[]) => resolve(notifications));
});
if (Platform.OS !== 'ios') {
return Promise.resolve([]);
}
return Notifications.ios
.getDeliveredNotifications()
.then(notifications =>
notifications.map(notification => normalizeNotificationPayload(notification, { foreground: true, userInteraction: false })),
);
} catch (error) {
console.error('Error getting delivered notifications:', error);
throw error;
@ -534,47 +706,19 @@ export const getDeliveredNotifications: () => Promise<Record<string, any>[]> = (
};
export const removeDeliveredNotifications = (identifiers = []) => {
PushNotification.removeDeliveredNotifications(identifiers);
if (Platform.OS === 'ios') {
Notifications.ios.removeDeliveredNotifications(identifiers);
}
};
export const setApplicationIconBadgeNumber = (badges: number) => {
PushNotification.setApplicationIconBadgeNumber(badges);
if (Platform.OS === 'ios') {
Notifications.ios.setBadgeCount(badges);
}
};
export const removeAllDeliveredNotifications = () => {
PushNotification.removeAllDeliveredNotifications();
};
export const getDefaultUri = () => {
return groundControlUri;
};
export const saveUri = async (uri: string) => {
try {
baseURI = uri || groundControlUri;
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, baseURI);
} catch (error) {
console.error('Error saving URI:', error);
throw error;
}
};
export const getSavedUri = async () => {
try {
const baseUriStored = await AsyncStorage.getItem(GROUNDCONTROL_BASE_URI);
if (baseUriStored) {
baseURI = baseUriStored;
}
return baseUriStored;
} catch (e) {
console.error(e);
try {
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, groundControlUri);
} catch (storageError) {
console.error('Failed to reset URI:', storageError);
}
throw e;
}
Notifications.removeAllDeliveredNotifications();
};
export const isNotificationsEnabled = async () => {
@ -615,25 +759,22 @@ export const getStoredNotifications = async (): Promise<TPayload[]> => {
// on app launch (load module):
export const initializeNotifications = async (onProcessNotifications?: () => void) => {
console.debug('initializeNotifications: Starting initialization');
console.log('initializeNotifications: Starting initialization');
try {
const noAndDontAskFlag = await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG);
console.debug('initializeNotifications: No ask flag status:', noAndDontAskFlag);
console.log('initializeNotifications: No ask flag status:', noAndDontAskFlag);
if (noAndDontAskFlag === 'true') {
console.warn('User has opted out of notifications.');
return;
}
const baseUriStored = await AsyncStorage.getItem(GROUNDCONTROL_BASE_URI);
baseURI = baseUriStored || groundControlUri;
console.debug('Base URI set to:', baseURI);
setApplicationIconBadgeNumber(0);
// Only check permissions, never request
currentPermissionStatus = await checkNotificationPermissionStatus();
console.debug('initializeNotifications: Permission status:', currentPermissionStatus);
console.log('initializeNotifications: Permission status:', currentPermissionStatus);
// Handle Android 13+ permissions differently
const canProceed =
@ -642,23 +783,12 @@ export const initializeNotifications = async (onProcessNotifications?: () => voi
: currentPermissionStatus === 'granted';
if (canProceed) {
console.debug('initializeNotifications: Can proceed with notification setup');
const token = await getPushToken();
if (token) {
console.debug('initializeNotifications: Existing token found, configuring');
await configureNotifications(onProcessNotifications);
await postTokenConfig();
} else {
console.debug('initializeNotifications: No token found, will request permissions');
await tryToObtainPermissions();
}
console.log('initializeNotifications: Can proceed with notification setup');
await configureNotifications(onProcessNotifications);
} else {
console.debug('Notifications require user action to enable');
console.log('Notifications require user action to enable');
}
} catch (error) {
console.error('Failed to initialize notifications:', error);
baseURI = groundControlUri;
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, groundControlUri).catch(err => console.error('Failed to reset URI:', err));
}
};

21
blue_modules/pako/LICENSE Normal file
View File

@ -0,0 +1,21 @@
(The MIT License)
Copyright (C) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

177
blue_modules/pako/README.md Normal file
View File

@ -0,0 +1,177 @@
pako
==========================================
[![CI](https://github.com/nodeca/pako/workflows/CI/badge.svg)](https://github.com/nodeca/pako/actions)
[![NPM version](https://img.shields.io/npm/v/pako.svg)](https://www.npmjs.org/package/pako)
> zlib port to javascript, very fast!
__Why pako is cool:__
- Results are binary equal to well known [zlib](http://www.zlib.net/) (now contains ported zlib v1.2.8).
- Almost as fast in modern JS engines as C implementation (see benchmarks).
- Works in browsers, you can browserify any separate component.
This project was done to understand how fast JS can be and is it necessary to
develop native C modules for CPU-intensive tasks. Enjoy the result!
__Benchmarks:__
node v12.16.3 (zlib 1.2.9), 1mb input sample:
```
deflate-imaya x 4.75 ops/sec ±4.93% (15 runs sampled)
deflate-pako x 10.38 ops/sec ±0.37% (29 runs sampled)
deflate-zlib x 17.74 ops/sec ±0.77% (46 runs sampled)
gzip-pako x 8.86 ops/sec ±1.41% (29 runs sampled)
inflate-imaya x 107 ops/sec ±0.69% (77 runs sampled)
inflate-pako x 131 ops/sec ±1.74% (82 runs sampled)
inflate-zlib x 258 ops/sec ±0.66% (88 runs sampled)
ungzip-pako x 115 ops/sec ±1.92% (80 runs sampled)
```
node v14.15.0 (google's zlib), 1mb output sample:
```
deflate-imaya x 4.93 ops/sec ±3.09% (16 runs sampled)
deflate-pako x 10.22 ops/sec ±0.33% (29 runs sampled)
deflate-zlib x 18.48 ops/sec ±0.24% (48 runs sampled)
gzip-pako x 10.16 ops/sec ±0.25% (28 runs sampled)
inflate-imaya x 110 ops/sec ±0.41% (77 runs sampled)
inflate-pako x 134 ops/sec ±0.66% (83 runs sampled)
inflate-zlib x 402 ops/sec ±0.74% (87 runs sampled)
ungzip-pako x 113 ops/sec ±0.62% (80 runs sampled)
```
zlib's test is partially affected by marshalling (that make sense for inflate only).
You can change deflate level to 0 in benchmark source, to investigate details.
For deflate level 6 results can be considered as correct.
__Install:__
```
npm install pako
```
Examples / API
--------------
Full docs - http://nodeca.github.io/pako/
```javascript
const pako = require('pako');
// Deflate
//
const input = new Uint8Array();
//... fill input data here
const output = pako.deflate(input);
// Inflate (simple wrapper can throw exception on broken stream)
//
const compressed = new Uint8Array();
//... fill data to uncompress here
try {
const result = pako.inflate(compressed);
// ... continue processing
} catch (err) {
console.log(err);
}
//
// Alternate interface for chunking & without exceptions
//
const deflator = new pako.Deflate();
deflator.push(chunk1, false);
deflator.push(chunk2); // second param is false by default.
...
deflator.push(chunk_last, true); // `true` says this chunk is last
if (deflator.err) {
console.log(deflator.msg);
}
const output = deflator.result;
const inflator = new pako.Inflate();
inflator.push(chunk1);
inflator.push(chunk2);
...
inflator.push(chunk_last); // no second param because end is auto-detected
if (inflator.err) {
console.log(inflator.msg);
}
const output = inflator.result;
```
Sometime you can wish to work with strings. For example, to send
stringified objects to server. Pako's deflate detects input data type, and
automatically recode strings to utf-8 prior to compress. Inflate has special
option, to say compressed data has utf-8 encoding and should be recoded to
javascript's utf-16.
```javascript
const pako = require('pako');
const test = { my: 'super', puper: [456, 567], awesome: 'pako' };
const compressed = pako.deflate(JSON.stringify(test));
const restored = JSON.parse(pako.inflate(compressed, { to: 'string' }));
```
Notes
-----
Pako does not contain some specific zlib functions:
- __deflate__ - methods `deflateCopy`, `deflateBound`, `deflateParams`,
`deflatePending`, `deflatePrime`, `deflateTune`.
- __inflate__ - methods `inflateCopy`, `inflateMark`,
`inflatePrime`, `inflateGetDictionary`, `inflateSync`, `inflateSyncPoint`, `inflateUndermine`.
- High level inflate/deflate wrappers (classes) may not support some flush
modes.
pako for enterprise
-------------------
Available as part of the Tidelift Subscription
The maintainers of pako and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-pako?utm_source=npm-pako&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
Authors
-------
- Andrey Tupitsin [@anrd83](https://github.com/andr83)
- Vitaly Puzrin [@puzrin](https://github.com/puzrin)
Personal thanks to:
- Vyacheslav Egorov ([@mraleph](https://github.com/mraleph)) for his awesome
tutorials about optimising JS code for v8, [IRHydra](http://mrale.ph/irhydra/)
tool and his advices.
- David Duponchel ([@dduponchel](https://github.com/dduponchel)) for help with
testing.
Original implementation (in C):
- [zlib](http://zlib.net/) by Jean-loup Gailly and Mark Adler.
License
-------
- MIT - all files, except `/lib/zlib` folder
- ZLIB - `/lib/zlib` content

4
blue_modules/pako/dist/pako.esm.mjs vendored Normal file
View File

@ -0,0 +1,4 @@
import * as pako from '../index.js';
export * from '../index.js';
export default pako;

View File

@ -0,0 +1,18 @@
// Top level file is just a mixin of submodules & constants
'use strict';
const { Deflate, deflate, deflateRaw, gzip } = require('./lib/deflate');
const { Inflate, inflate, inflateRaw, ungzip } = require('./lib/inflate');
const constants = require('./lib/zlib/constants');
module.exports.Deflate = Deflate;
module.exports.deflate = deflate;
module.exports.deflateRaw = deflateRaw;
module.exports.gzip = gzip;
module.exports.Inflate = Inflate;
module.exports.inflate = inflate;
module.exports.inflateRaw = inflateRaw;
module.exports.ungzip = ungzip;
module.exports.constants = constants;

View File

@ -0,0 +1,380 @@
'use strict';
const zlib_deflate = require('./zlib/deflate');
const utils = require('./utils/common');
const strings = require('./utils/strings');
const msg = require('./zlib/messages');
const ZStream = require('./zlib/zstream');
const toString = Object.prototype.toString;
/* Public constants ==========================================================*/
/* ===========================================================================*/
const {
Z_NO_FLUSH, Z_SYNC_FLUSH, Z_FULL_FLUSH, Z_FINISH,
Z_OK, Z_STREAM_END,
Z_DEFAULT_COMPRESSION,
Z_DEFAULT_STRATEGY,
Z_DEFLATED
} = require('./zlib/constants');
/* ===========================================================================*/
/**
* class Deflate
*
* Generic JS-style wrapper for zlib calls. If you don't need
* streaming behaviour - use more simple functions: [[deflate]],
* [[deflateRaw]] and [[gzip]].
**/
/* internal
* Deflate.chunks -> Array
*
* Chunks of output data, if [[Deflate#onData]] not overridden.
**/
/**
* Deflate.result -> Uint8Array
*
* Compressed result, generated by default [[Deflate#onData]]
* and [[Deflate#onEnd]] handlers. Filled after you push last chunk
* (call [[Deflate#push]] with `Z_FINISH` / `true` param).
**/
/**
* Deflate.err -> Number
*
* Error code after deflate finished. 0 (Z_OK) on success.
* You will not need it in real life, because deflate errors
* are possible only on wrong options or bad `onData` / `onEnd`
* custom handlers.
**/
/**
* Deflate.msg -> String
*
* Error message, if [[Deflate.err]] != 0
**/
/**
* new Deflate(options)
* - options (Object): zlib deflate options.
*
* Creates new deflator instance with specified params. Throws exception
* on bad params. Supported options:
*
* - `level`
* - `windowBits`
* - `memLevel`
* - `strategy`
* - `dictionary`
*
* [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
* for more information on these.
*
* Additional options, for internal needs:
*
* - `chunkSize` - size of generated data chunks (16K by default)
* - `raw` (Boolean) - do raw deflate
* - `gzip` (Boolean) - create gzip wrapper
* - `header` (Object) - custom header for gzip
* - `text` (Boolean) - true if compressed data believed to be text
* - `time` (Number) - modification time, unix timestamp
* - `os` (Number) - operation system code
* - `extra` (Array) - array of bytes with extra data (max 65536)
* - `name` (String) - file name (binary string)
* - `comment` (String) - comment (binary string)
* - `hcrc` (Boolean) - true if header crc should be added
*
* ##### Example:
*
* ```javascript
* const pako = require('pako')
* , chunk1 = new Uint8Array([1,2,3,4,5,6,7,8,9])
* , chunk2 = new Uint8Array([10,11,12,13,14,15,16,17,18,19]);
*
* const deflate = new pako.Deflate({ level: 3});
*
* deflate.push(chunk1, false);
* deflate.push(chunk2, true); // true -> last chunk
*
* if (deflate.err) { throw new Error(deflate.err); }
*
* console.log(deflate.result);
* ```
**/
function Deflate(options) {
this.options = utils.assign({
level: Z_DEFAULT_COMPRESSION,
method: Z_DEFLATED,
chunkSize: 16384,
windowBits: 15,
memLevel: 8,
strategy: Z_DEFAULT_STRATEGY
}, options || {});
let opt = this.options;
if (opt.raw && (opt.windowBits > 0)) {
opt.windowBits = -opt.windowBits;
}
else if (opt.gzip && (opt.windowBits > 0) && (opt.windowBits < 16)) {
opt.windowBits += 16;
}
this.err = 0; // error code, if happens (0 = Z_OK)
this.msg = ''; // error message
this.ended = false; // used to avoid multiple onEnd() calls
this.chunks = []; // chunks of compressed data
this.strm = new ZStream();
this.strm.avail_out = 0;
let status = zlib_deflate.deflateInit2(
this.strm,
opt.level,
opt.method,
opt.windowBits,
opt.memLevel,
opt.strategy
);
if (status !== Z_OK) {
throw new Error(msg[status]);
}
if (opt.header) {
zlib_deflate.deflateSetHeader(this.strm, opt.header);
}
if (opt.dictionary) {
let dict;
// Convert data if needed
if (typeof opt.dictionary === 'string') {
// If we need to compress text, change encoding to utf8.
dict = strings.string2buf(opt.dictionary);
} else if (toString.call(opt.dictionary) === '[object ArrayBuffer]') {
dict = new Uint8Array(opt.dictionary);
} else {
dict = opt.dictionary;
}
status = zlib_deflate.deflateSetDictionary(this.strm, dict);
if (status !== Z_OK) {
throw new Error(msg[status]);
}
this._dict_set = true;
}
}
/**
* Deflate#push(data[, flush_mode]) -> Boolean
* - data (Uint8Array|ArrayBuffer|String): input data. Strings will be
* converted to utf8 byte sequence.
* - flush_mode (Number|Boolean): 0..6 for corresponding Z_NO_FLUSH..Z_TREE modes.
* See constants. Skipped or `false` means Z_NO_FLUSH, `true` means Z_FINISH.
*
* Sends input data to deflate pipe, generating [[Deflate#onData]] calls with
* new compressed chunks. Returns `true` on success. The last data block must
* have `flush_mode` Z_FINISH (or `true`). That will flush internal pending
* buffers and call [[Deflate#onEnd]].
*
* On fail call [[Deflate#onEnd]] with error code and return false.
*
* ##### Example
*
* ```javascript
* push(chunk, false); // push one of data chunks
* ...
* push(chunk, true); // push last chunk
* ```
**/
Deflate.prototype.push = function (data, flush_mode) {
const strm = this.strm;
const chunkSize = this.options.chunkSize;
let status, _flush_mode;
if (this.ended) { return false; }
if (flush_mode === ~~flush_mode) _flush_mode = flush_mode;
else _flush_mode = flush_mode === true ? Z_FINISH : Z_NO_FLUSH;
// Convert data if needed
if (typeof data === 'string') {
// If we need to compress text, change encoding to utf8.
strm.input = strings.string2buf(data);
} else if (toString.call(data) === '[object ArrayBuffer]') {
strm.input = new Uint8Array(data);
} else {
strm.input = data;
}
strm.next_in = 0;
strm.avail_in = strm.input.length;
for (;;) {
if (strm.avail_out === 0) {
strm.output = new Uint8Array(chunkSize);
strm.next_out = 0;
strm.avail_out = chunkSize;
}
// Make sure avail_out > 6 to avoid repeating markers
if ((_flush_mode === Z_SYNC_FLUSH || _flush_mode === Z_FULL_FLUSH) && strm.avail_out <= 6) {
this.onData(strm.output.subarray(0, strm.next_out));
strm.avail_out = 0;
continue;
}
status = zlib_deflate.deflate(strm, _flush_mode);
// Ended => flush and finish
if (status === Z_STREAM_END) {
if (strm.next_out > 0) {
this.onData(strm.output.subarray(0, strm.next_out));
}
status = zlib_deflate.deflateEnd(this.strm);
this.onEnd(status);
this.ended = true;
return status === Z_OK;
}
// Flush if out buffer full
if (strm.avail_out === 0) {
this.onData(strm.output);
continue;
}
// Flush if requested and has data
if (_flush_mode > 0 && strm.next_out > 0) {
this.onData(strm.output.subarray(0, strm.next_out));
strm.avail_out = 0;
continue;
}
if (strm.avail_in === 0) break;
}
return true;
};
/**
* Deflate#onData(chunk) -> Void
* - chunk (Uint8Array): output data.
*
* By default, stores data blocks in `chunks[]` property and glue
* those in `onEnd`. Override this handler, if you need another behaviour.
**/
Deflate.prototype.onData = function (chunk) {
this.chunks.push(chunk);
};
/**
* Deflate#onEnd(status) -> Void
* - status (Number): deflate status. 0 (Z_OK) on success,
* other if not.
*
* Called once after you tell deflate that the input stream is
* complete (Z_FINISH). By default - join collected chunks,
* free memory and fill `results` / `err` properties.
**/
Deflate.prototype.onEnd = function (status) {
// On success - join
if (status === Z_OK) {
this.result = utils.flattenChunks(this.chunks);
}
this.chunks = [];
this.err = status;
this.msg = this.strm.msg;
};
/**
* deflate(data[, options]) -> Uint8Array
* - data (Uint8Array|ArrayBuffer|String): input data to compress.
* - options (Object): zlib deflate options.
*
* Compress `data` with deflate algorithm and `options`.
*
* Supported options are:
*
* - level
* - windowBits
* - memLevel
* - strategy
* - dictionary
*
* [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
* for more information on these.
*
* Sugar (options):
*
* - `raw` (Boolean) - say that we work with raw stream, if you don't wish to specify
* negative windowBits implicitly.
*
* ##### Example:
*
* ```javascript
* const pako = require('pako')
* const data = new Uint8Array([1,2,3,4,5,6,7,8,9]);
*
* console.log(pako.deflate(data));
* ```
**/
function deflate(input, options) {
const deflator = new Deflate(options);
deflator.push(input, true);
// That will never happens, if you don't cheat with options :)
if (deflator.err) { throw deflator.msg || msg[deflator.err]; }
return deflator.result;
}
/**
* deflateRaw(data[, options]) -> Uint8Array
* - data (Uint8Array|ArrayBuffer|String): input data to compress.
* - options (Object): zlib deflate options.
*
* The same as [[deflate]], but creates raw data, without wrapper
* (header and adler32 crc).
**/
function deflateRaw(input, options) {
options = options || {};
options.raw = true;
return deflate(input, options);
}
/**
* gzip(data[, options]) -> Uint8Array
* - data (Uint8Array|ArrayBuffer|String): input data to compress.
* - options (Object): zlib deflate options.
*
* The same as [[deflate]], but create gzip wrapper instead of
* deflate one.
**/
function gzip(input, options) {
options = options || {};
options.gzip = true;
return deflate(input, options);
}
module.exports.Deflate = Deflate;
module.exports.deflate = deflate;
module.exports.deflateRaw = deflateRaw;
module.exports.gzip = gzip;
module.exports.constants = require('./zlib/constants');

View File

@ -0,0 +1,419 @@
'use strict';
const zlib_inflate = require('./zlib/inflate');
const utils = require('./utils/common');
const strings = require('./utils/strings');
const msg = require('./zlib/messages');
const ZStream = require('./zlib/zstream');
const GZheader = require('./zlib/gzheader');
const toString = Object.prototype.toString;
/* Public constants ==========================================================*/
/* ===========================================================================*/
const {
Z_NO_FLUSH, Z_FINISH,
Z_OK, Z_STREAM_END, Z_NEED_DICT, Z_STREAM_ERROR, Z_DATA_ERROR, Z_MEM_ERROR
} = require('./zlib/constants');
/* ===========================================================================*/
/**
* class Inflate
*
* Generic JS-style wrapper for zlib calls. If you don't need
* streaming behaviour - use more simple functions: [[inflate]]
* and [[inflateRaw]].
**/
/* internal
* inflate.chunks -> Array
*
* Chunks of output data, if [[Inflate#onData]] not overridden.
**/
/**
* Inflate.result -> Uint8Array|String
*
* Uncompressed result, generated by default [[Inflate#onData]]
* and [[Inflate#onEnd]] handlers. Filled after you push last chunk
* (call [[Inflate#push]] with `Z_FINISH` / `true` param).
**/
/**
* Inflate.err -> Number
*
* Error code after inflate finished. 0 (Z_OK) on success.
* Should be checked if broken data possible.
**/
/**
* Inflate.msg -> String
*
* Error message, if [[Inflate.err]] != 0
**/
/**
* new Inflate(options)
* - options (Object): zlib inflate options.
*
* Creates new inflator instance with specified params. Throws exception
* on bad params. Supported options:
*
* - `windowBits`
* - `dictionary`
*
* [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
* for more information on these.
*
* Additional options, for internal needs:
*
* - `chunkSize` - size of generated data chunks (16K by default)
* - `raw` (Boolean) - do raw inflate
* - `to` (String) - if equal to 'string', then result will be converted
* from utf8 to utf16 (javascript) string. When string output requested,
* chunk length can differ from `chunkSize`, depending on content.
*
* By default, when no options set, autodetect deflate/gzip data format via
* wrapper header.
*
* ##### Example:
*
* ```javascript
* const pako = require('pako')
* const chunk1 = new Uint8Array([1,2,3,4,5,6,7,8,9])
* const chunk2 = new Uint8Array([10,11,12,13,14,15,16,17,18,19]);
*
* const inflate = new pako.Inflate({ level: 3});
*
* inflate.push(chunk1, false);
* inflate.push(chunk2, true); // true -> last chunk
*
* if (inflate.err) { throw new Error(inflate.err); }
*
* console.log(inflate.result);
* ```
**/
function Inflate(options) {
this.options = utils.assign({
chunkSize: 1024 * 64,
windowBits: 15,
to: ''
}, options || {});
const opt = this.options;
// Force window size for `raw` data, if not set directly,
// because we have no header for autodetect.
if (opt.raw && (opt.windowBits >= 0) && (opt.windowBits < 16)) {
opt.windowBits = -opt.windowBits;
if (opt.windowBits === 0) { opt.windowBits = -15; }
}
// If `windowBits` not defined (and mode not raw) - set autodetect flag for gzip/deflate
if ((opt.windowBits >= 0) && (opt.windowBits < 16) &&
!(options && options.windowBits)) {
opt.windowBits += 32;
}
// Gzip header has no info about windows size, we can do autodetect only
// for deflate. So, if window size not set, force it to max when gzip possible
if ((opt.windowBits > 15) && (opt.windowBits < 48)) {
// bit 3 (16) -> gzipped data
// bit 4 (32) -> autodetect gzip/deflate
if ((opt.windowBits & 15) === 0) {
opt.windowBits |= 15;
}
}
this.err = 0; // error code, if happens (0 = Z_OK)
this.msg = ''; // error message
this.ended = false; // used to avoid multiple onEnd() calls
this.chunks = []; // chunks of compressed data
this.strm = new ZStream();
this.strm.avail_out = 0;
let status = zlib_inflate.inflateInit2(
this.strm,
opt.windowBits
);
if (status !== Z_OK) {
throw new Error(msg[status]);
}
this.header = new GZheader();
zlib_inflate.inflateGetHeader(this.strm, this.header);
// Setup dictionary
if (opt.dictionary) {
// Convert data if needed
if (typeof opt.dictionary === 'string') {
opt.dictionary = strings.string2buf(opt.dictionary);
} else if (toString.call(opt.dictionary) === '[object ArrayBuffer]') {
opt.dictionary = new Uint8Array(opt.dictionary);
}
if (opt.raw) { //In raw mode we need to set the dictionary early
status = zlib_inflate.inflateSetDictionary(this.strm, opt.dictionary);
if (status !== Z_OK) {
throw new Error(msg[status]);
}
}
}
}
/**
* Inflate#push(data[, flush_mode]) -> Boolean
* - data (Uint8Array|ArrayBuffer): input data
* - flush_mode (Number|Boolean): 0..6 for corresponding Z_NO_FLUSH..Z_TREE
* flush modes. See constants. Skipped or `false` means Z_NO_FLUSH,
* `true` means Z_FINISH.
*
* Sends input data to inflate pipe, generating [[Inflate#onData]] calls with
* new output chunks. Returns `true` on success. If end of stream detected,
* [[Inflate#onEnd]] will be called.
*
* `flush_mode` is not needed for normal operation, because end of stream
* detected automatically. You may try to use it for advanced things, but
* this functionality was not tested.
*
* On fail call [[Inflate#onEnd]] with error code and return false.
*
* ##### Example
*
* ```javascript
* push(chunk, false); // push one of data chunks
* ...
* push(chunk, true); // push last chunk
* ```
**/
Inflate.prototype.push = function (data, flush_mode) {
const strm = this.strm;
const chunkSize = this.options.chunkSize;
const dictionary = this.options.dictionary;
let status, _flush_mode, last_avail_out;
if (this.ended) return false;
if (flush_mode === ~~flush_mode) _flush_mode = flush_mode;
else _flush_mode = flush_mode === true ? Z_FINISH : Z_NO_FLUSH;
// Convert data if needed
if (toString.call(data) === '[object ArrayBuffer]') {
strm.input = new Uint8Array(data);
} else {
strm.input = data;
}
strm.next_in = 0;
strm.avail_in = strm.input.length;
for (;;) {
if (strm.avail_out === 0) {
strm.output = new Uint8Array(chunkSize);
strm.next_out = 0;
strm.avail_out = chunkSize;
}
status = zlib_inflate.inflate(strm, _flush_mode);
if (status === Z_NEED_DICT && dictionary) {
status = zlib_inflate.inflateSetDictionary(strm, dictionary);
if (status === Z_OK) {
status = zlib_inflate.inflate(strm, _flush_mode);
} else if (status === Z_DATA_ERROR) {
// Replace code with more verbose
status = Z_NEED_DICT;
}
}
// Skip snyc markers if more data follows and not raw mode
while (strm.avail_in > 0 &&
status === Z_STREAM_END &&
strm.state.wrap > 0 &&
data[strm.next_in] !== 0)
{
zlib_inflate.inflateReset(strm);
status = zlib_inflate.inflate(strm, _flush_mode);
}
switch (status) {
case Z_STREAM_ERROR:
case Z_DATA_ERROR:
case Z_NEED_DICT:
case Z_MEM_ERROR:
this.onEnd(status);
this.ended = true;
return false;
}
// Remember real `avail_out` value, because we may patch out buffer content
// to align utf8 strings boundaries.
last_avail_out = strm.avail_out;
if (strm.next_out) {
if (strm.avail_out === 0 || status === Z_STREAM_END) {
if (this.options.to === 'string') {
let next_out_utf8 = strings.utf8border(strm.output, strm.next_out);
let tail = strm.next_out - next_out_utf8;
let utf8str = strings.buf2string(strm.output, next_out_utf8);
// move tail & realign counters
strm.next_out = tail;
strm.avail_out = chunkSize - tail;
if (tail) strm.output.set(strm.output.subarray(next_out_utf8, next_out_utf8 + tail), 0);
this.onData(utf8str);
} else {
this.onData(strm.output.length === strm.next_out ? strm.output : strm.output.subarray(0, strm.next_out));
}
}
}
// Must repeat iteration if out buffer is full
if (status === Z_OK && last_avail_out === 0) continue;
// Finalize if end of stream reached.
if (status === Z_STREAM_END) {
status = zlib_inflate.inflateEnd(this.strm);
this.onEnd(status);
this.ended = true;
return true;
}
if (strm.avail_in === 0) break;
}
return true;
};
/**
* Inflate#onData(chunk) -> Void
* - chunk (Uint8Array|String): output data. When string output requested,
* each chunk will be string.
*
* By default, stores data blocks in `chunks[]` property and glue
* those in `onEnd`. Override this handler, if you need another behaviour.
**/
Inflate.prototype.onData = function (chunk) {
this.chunks.push(chunk);
};
/**
* Inflate#onEnd(status) -> Void
* - status (Number): inflate status. 0 (Z_OK) on success,
* other if not.
*
* Called either after you tell inflate that the input stream is
* complete (Z_FINISH). By default - join collected chunks,
* free memory and fill `results` / `err` properties.
**/
Inflate.prototype.onEnd = function (status) {
// On success - join
if (status === Z_OK) {
if (this.options.to === 'string') {
this.result = this.chunks.join('');
} else {
this.result = utils.flattenChunks(this.chunks);
}
}
this.chunks = [];
this.err = status;
this.msg = this.strm.msg;
};
/**
* inflate(data[, options]) -> Uint8Array|String
* - data (Uint8Array|ArrayBuffer): input data to decompress.
* - options (Object): zlib inflate options.
*
* Decompress `data` with inflate/ungzip and `options`. Autodetect
* format via wrapper header by default. That's why we don't provide
* separate `ungzip` method.
*
* Supported options are:
*
* - windowBits
*
* [http://zlib.net/manual.html#Advanced](http://zlib.net/manual.html#Advanced)
* for more information.
*
* Sugar (options):
*
* - `raw` (Boolean) - say that we work with raw stream, if you don't wish to specify
* negative windowBits implicitly.
* - `to` (String) - if equal to 'string', then result will be converted
* from utf8 to utf16 (javascript) string. When string output requested,
* chunk length can differ from `chunkSize`, depending on content.
*
*
* ##### Example:
*
* ```javascript
* const pako = require('pako');
* const input = pako.deflate(new Uint8Array([1,2,3,4,5,6,7,8,9]));
* let output;
*
* try {
* output = pako.inflate(input);
* } catch (err) {
* console.log(err);
* }
* ```
**/
function inflate(input, options) {
const inflator = new Inflate(options);
inflator.push(input);
// That will never happens, if you don't cheat with options :)
if (inflator.err) throw inflator.msg || msg[inflator.err];
return inflator.result;
}
/**
* inflateRaw(data[, options]) -> Uint8Array|String
* - data (Uint8Array|ArrayBuffer): input data to decompress.
* - options (Object): zlib inflate options.
*
* The same as [[inflate]], but creates raw data, without wrapper
* (header and adler32 crc).
**/
function inflateRaw(input, options) {
options = options || {};
options.raw = true;
return inflate(input, options);
}
/**
* ungzip(data[, options]) -> Uint8Array|String
* - data (Uint8Array|ArrayBuffer): input data to decompress.
* - options (Object): zlib inflate options.
*
* Just shortcut to [[inflate]], because it autodetects format
* by header.content. Done for convenience.
**/
module.exports.Inflate = Inflate;
module.exports.inflate = inflate;
module.exports.inflateRaw = inflateRaw;
module.exports.ungzip = inflate;
module.exports.constants = require('./zlib/constants');

View File

@ -0,0 +1,48 @@
'use strict';
const _has = (obj, key) => {
return Object.prototype.hasOwnProperty.call(obj, key);
};
module.exports.assign = function (obj /*from1, from2, from3, ...*/) {
const sources = Array.prototype.slice.call(arguments, 1);
while (sources.length) {
const source = sources.shift();
if (!source) { continue; }
if (typeof source !== 'object') {
throw new TypeError(source + 'must be non-object');
}
for (const p in source) {
if (_has(source, p)) {
obj[p] = source[p];
}
}
}
return obj;
};
// Join array of chunks to single array.
module.exports.flattenChunks = (chunks) => {
// calculate data length
let len = 0;
for (let i = 0, l = chunks.length; i < l; i++) {
len += chunks[i].length;
}
// join chunks
const result = new Uint8Array(len);
for (let i = 0, pos = 0, l = chunks.length; i < l; i++) {
let chunk = chunks[i];
result.set(chunk, pos);
pos += chunk.length;
}
return result;
};

View File

@ -0,0 +1,174 @@
// String encode/decode helpers
'use strict';
// Quick check if we can use fast array to bin string conversion
//
// - apply(Array) can fail on Android 2.2
// - apply(Uint8Array) can fail on iOS 5.1 Safari
//
let STR_APPLY_UIA_OK = true;
try { String.fromCharCode.apply(null, new Uint8Array(1)); } catch (__) { STR_APPLY_UIA_OK = false; }
// Table with utf8 lengths (calculated by first byte of sequence)
// Note, that 5 & 6-byte values and some 4-byte values can not be represented in JS,
// because max possible codepoint is 0x10ffff
const _utf8len = new Uint8Array(256);
for (let q = 0; q < 256; q++) {
_utf8len[q] = (q >= 252 ? 6 : q >= 248 ? 5 : q >= 240 ? 4 : q >= 224 ? 3 : q >= 192 ? 2 : 1);
}
_utf8len[254] = _utf8len[254] = 1; // Invalid sequence start
// convert string to array (typed, when possible)
module.exports.string2buf = (str) => {
if (typeof TextEncoder === 'function' && TextEncoder.prototype.encode) {
return new TextEncoder().encode(str);
}
let buf, c, c2, m_pos, i, str_len = str.length, buf_len = 0;
// count binary size
for (m_pos = 0; m_pos < str_len; m_pos++) {
c = str.charCodeAt(m_pos);
if ((c & 0xfc00) === 0xd800 && (m_pos + 1 < str_len)) {
c2 = str.charCodeAt(m_pos + 1);
if ((c2 & 0xfc00) === 0xdc00) {
c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
m_pos++;
}
}
buf_len += c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : 4;
}
// allocate buffer
buf = new Uint8Array(buf_len);
// convert
for (i = 0, m_pos = 0; i < buf_len; m_pos++) {
c = str.charCodeAt(m_pos);
if ((c & 0xfc00) === 0xd800 && (m_pos + 1 < str_len)) {
c2 = str.charCodeAt(m_pos + 1);
if ((c2 & 0xfc00) === 0xdc00) {
c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
m_pos++;
}
}
if (c < 0x80) {
/* one byte */
buf[i++] = c;
} else if (c < 0x800) {
/* two bytes */
buf[i++] = 0xC0 | (c >>> 6);
buf[i++] = 0x80 | (c & 0x3f);
} else if (c < 0x10000) {
/* three bytes */
buf[i++] = 0xE0 | (c >>> 12);
buf[i++] = 0x80 | (c >>> 6 & 0x3f);
buf[i++] = 0x80 | (c & 0x3f);
} else {
/* four bytes */
buf[i++] = 0xf0 | (c >>> 18);
buf[i++] = 0x80 | (c >>> 12 & 0x3f);
buf[i++] = 0x80 | (c >>> 6 & 0x3f);
buf[i++] = 0x80 | (c & 0x3f);
}
}
return buf;
};
// Helper
const buf2binstring = (buf, len) => {
// On Chrome, the arguments in a function call that are allowed is `65534`.
// If the length of the buffer is smaller than that, we can use this optimization,
// otherwise we will take a slower path.
if (len < 65534) {
if (buf.subarray && STR_APPLY_UIA_OK) {
return String.fromCharCode.apply(null, buf.length === len ? buf : buf.subarray(0, len));
}
}
let result = '';
for (let i = 0; i < len; i++) {
result += String.fromCharCode(buf[i]);
}
return result;
};
// convert array to string
module.exports.buf2string = (buf, max) => {
const len = max || buf.length;
if (typeof TextDecoder === 'function' && TextDecoder.prototype.decode) {
return new TextDecoder().decode(buf.subarray(0, max));
}
let i, out;
// Reserve max possible length (2 words per char)
// NB: by unknown reasons, Array is significantly faster for
// String.fromCharCode.apply than Uint16Array.
const utf16buf = new Array(len * 2);
for (out = 0, i = 0; i < len;) {
let c = buf[i++];
// quick process ascii
if (c < 0x80) { utf16buf[out++] = c; continue; }
let c_len = _utf8len[c];
// skip 5 & 6 byte codes
if (c_len > 4) { utf16buf[out++] = 0xfffd; i += c_len - 1; continue; }
// apply mask on first byte
c &= c_len === 2 ? 0x1f : c_len === 3 ? 0x0f : 0x07;
// join the rest
while (c_len > 1 && i < len) {
c = (c << 6) | (buf[i++] & 0x3f);
c_len--;
}
// terminated by end of string?
if (c_len > 1) { utf16buf[out++] = 0xfffd; continue; }
if (c < 0x10000) {
utf16buf[out++] = c;
} else {
c -= 0x10000;
utf16buf[out++] = 0xd800 | ((c >> 10) & 0x3ff);
utf16buf[out++] = 0xdc00 | (c & 0x3ff);
}
}
return buf2binstring(utf16buf, out);
};
// Calculate max possible position in utf8 buffer,
// that will not break sequence. If that's not possible
// - (very small limits) return max size as is.
//
// buf[] - utf8 bytes array
// max - length limit (mandatory);
module.exports.utf8border = (buf, max) => {
max = max || buf.length;
if (max > buf.length) { max = buf.length; }
// go back from last position, until start of sequence found
let pos = max - 1;
while (pos >= 0 && (buf[pos] & 0xC0) === 0x80) { pos--; }
// Very small and broken sequence,
// return max, because we should return something anyway.
if (pos < 0) { return max; }
// If we came to start of buffer - that means buffer is too small,
// return max too.
if (pos === 0) { return max; }
return (pos + _utf8len[buf[pos]] > max) ? pos : max;
};

View File

@ -0,0 +1,59 @@
Content of this folder follows zlib C sources as close as possible.
That's intended to simplify maintainability and guarantee equal API
and result.
Key differences:
- Everything is in JavaScript.
- No platform-dependent blocks.
- Some things like crc32 rewritten to keep size small and make JIT
work better.
- Some code is different due missed features in JS (macros, pointers,
structures, header files)
- Specific API methods are not implemented (see notes in root readme)
This port is based on zlib 1.2.8.
This port is under zlib license (see below) with contribution and addition of javascript
port under expat license (see LICENSE at root of project)
Copyright:
(C) 1995-2013 Jean-loup Gailly and Mark Adler
(C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
From zlib's README
=============================================================================
Acknowledgments:
The deflate format used by zlib was defined by Phil Katz. The deflate and
zlib specifications were written by L. Peter Deutsch. Thanks to all the
people who reported problems and suggested various improvements in zlib; they
are too numerous to cite here.
Copyright notice:
(C) 1995-2013 Jean-loup Gailly and Mark Adler
Copyright (c) <''year''> <''copyright holders''>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
Jean-loup Gailly Mark Adler
jloup@gzip.org madler@alumni.caltech.edu

View File

@ -0,0 +1,51 @@
'use strict';
// Note: adler32 takes 12% for level 0 and 2% for level 6.
// It isn't worth it to make additional optimizations as in original.
// Small size is preferable.
// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.
const adler32 = (adler, buf, len, pos) => {
let s1 = (adler & 0xffff) |0,
s2 = ((adler >>> 16) & 0xffff) |0,
n = 0;
while (len !== 0) {
// Set limit ~ twice less than 5552, to keep
// s2 in 31-bits, because we force signed ints.
// in other case %= will fail.
n = len > 2000 ? 2000 : len;
len -= n;
do {
s1 = (s1 + buf[pos++]) |0;
s2 = (s2 + s1) |0;
} while (--n);
s1 %= 65521;
s2 %= 65521;
}
return (s1 | (s2 << 16)) |0;
};
module.exports = adler32;

View File

@ -0,0 +1,68 @@
'use strict';
// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.
module.exports = {
/* Allowed flush values; see deflate() and inflate() below for details */
Z_NO_FLUSH: 0,
Z_PARTIAL_FLUSH: 1,
Z_SYNC_FLUSH: 2,
Z_FULL_FLUSH: 3,
Z_FINISH: 4,
Z_BLOCK: 5,
Z_TREES: 6,
/* Return codes for the compression/decompression functions. Negative values
* are errors, positive values are used for special but normal events.
*/
Z_OK: 0,
Z_STREAM_END: 1,
Z_NEED_DICT: 2,
Z_ERRNO: -1,
Z_STREAM_ERROR: -2,
Z_DATA_ERROR: -3,
Z_MEM_ERROR: -4,
Z_BUF_ERROR: -5,
//Z_VERSION_ERROR: -6,
/* compression levels */
Z_NO_COMPRESSION: 0,
Z_BEST_SPEED: 1,
Z_BEST_COMPRESSION: 9,
Z_DEFAULT_COMPRESSION: -1,
Z_FILTERED: 1,
Z_HUFFMAN_ONLY: 2,
Z_RLE: 3,
Z_FIXED: 4,
Z_DEFAULT_STRATEGY: 0,
/* Possible values of the data_type field (though see inflate()) */
Z_BINARY: 0,
Z_TEXT: 1,
//Z_ASCII: 1, // = Z_TEXT (deprecated)
Z_UNKNOWN: 2,
/* The deflate compression method */
Z_DEFLATED: 8
//Z_NULL: null // Use -1 or null inline, depending on var type
};

View File

@ -0,0 +1,59 @@
'use strict';
// Note: we can't get significant speed boost here.
// So write code to minimize size - no pregenerated tables
// and array tools dependencies.
// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.
// Use ordinary array, since untyped makes no boost here
const makeTable = () => {
let c, table = [];
for (var n = 0; n < 256; n++) {
c = n;
for (var k = 0; k < 8; k++) {
c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
}
table[n] = c;
}
return table;
};
// Create table on load. Just 255 signed longs. Not a problem.
const crcTable = new Uint32Array(makeTable());
const crc32 = (crc, buf, len, pos) => {
const t = crcTable;
const end = pos + len;
crc ^= -1;
for (let i = pos; i < end; i++) {
crc = (crc >>> 8) ^ t[(crc ^ buf[i]) & 0xFF];
}
return (crc ^ (-1)); // >>> 0;
};
module.exports = crc32;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
'use strict';
// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.
function GZheader() {
/* true if compressed data believed to be text */
this.text = 0;
/* modification time */
this.time = 0;
/* extra flags (not used when writing a gzip file) */
this.xflags = 0;
/* operating system */
this.os = 0;
/* pointer to extra field or Z_NULL if none */
this.extra = null;
/* extra field length (valid if extra != Z_NULL) */
this.extra_len = 0; // Actually, we don't need it in JS,
// but leave for few code modifications
//
// Setup limits is not necessary because in js we should not preallocate memory
// for inflate use constant limit in 65536 bytes
//
/* space at extra (only when reading header) */
// this.extra_max = 0;
/* pointer to zero-terminated file name or Z_NULL */
this.name = '';
/* space at name (only when reading header) */
// this.name_max = 0;
/* pointer to zero-terminated comment or Z_NULL */
this.comment = '';
/* space at comment (only when reading header) */
// this.comm_max = 0;
/* true if there was or will be a header crc */
this.hcrc = 0;
/* true when done reading gzip header (not used when writing a gzip file) */
this.done = false;
}
module.exports = GZheader;

View File

@ -0,0 +1,344 @@
'use strict';
// (C) 1995-2013 Jean-loup Gailly and Mark Adler
// (C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.
// See state defs from inflate.js
const BAD = 16209; /* got a data error -- remain here until reset */
const TYPE = 16191; /* i: waiting for type bits, including last-flag bit */
/*
Decode literal, length, and distance codes and write out the resulting
literal and match bytes until either not enough input or output is
available, an end-of-block is encountered, or a data error is encountered.
When large enough input and output buffers are supplied to inflate(), for
example, a 16K input buffer and a 64K output buffer, more than 95% of the
inflate execution time is spent in this routine.
Entry assumptions:
state.mode === LEN
strm.avail_in >= 6
strm.avail_out >= 258
start >= strm.avail_out
state.bits < 8
On return, state.mode is one of:
LEN -- ran out of enough output space or enough available input
TYPE -- reached end of block code, inflate() to interpret next block
BAD -- error in block data
Notes:
- The maximum input bits used by a length/distance pair is 15 bits for the
length code, 5 bits for the length extra, 15 bits for the distance code,
and 13 bits for the distance extra. This totals 48 bits, or six bytes.
Therefore if strm.avail_in >= 6, then there is enough input to avoid
checking for available input while decoding.
- The maximum bytes that a single length/distance pair can output is 258
bytes, which is the maximum length that can be coded. inflate_fast()
requires strm.avail_out >= 258 for each loop to avoid checking for
output space.
*/
module.exports = function inflate_fast(strm, start) {
let _in; /* local strm.input */
let last; /* have enough input while in < last */
let _out; /* local strm.output */
let beg; /* inflate()'s initial strm.output */
let end; /* while out < end, enough space available */
//#ifdef INFLATE_STRICT
let dmax; /* maximum distance from zlib header */
//#endif
let wsize; /* window size or zero if not using window */
let whave; /* valid bytes in the window */
let wnext; /* window write index */
// Use `s_window` instead `window`, avoid conflict with instrumentation tools
let s_window; /* allocated sliding window, if wsize != 0 */
let hold; /* local strm.hold */
let bits; /* local strm.bits */
let lcode; /* local strm.lencode */
let dcode; /* local strm.distcode */
let lmask; /* mask for first level of length codes */
let dmask; /* mask for first level of distance codes */
let here; /* retrieved table entry */
let op; /* code bits, operation, extra bits, or */
/* window position, window bytes to copy */
let len; /* match length, unused bytes */
let dist; /* match distance */
let from; /* where to copy match from */
let from_source;
let input, output; // JS specific, because we have no pointers
/* copy state to local variables */
const state = strm.state;
//here = state.here;
_in = strm.next_in;
input = strm.input;
last = _in + (strm.avail_in - 5);
_out = strm.next_out;
output = strm.output;
beg = _out - (start - strm.avail_out);
end = _out + (strm.avail_out - 257);
//#ifdef INFLATE_STRICT
dmax = state.dmax;
//#endif
wsize = state.wsize;
whave = state.whave;
wnext = state.wnext;
s_window = state.window;
hold = state.hold;
bits = state.bits;
lcode = state.lencode;
dcode = state.distcode;
lmask = (1 << state.lenbits) - 1;
dmask = (1 << state.distbits) - 1;
/* decode literals and length/distances until end-of-block or not enough
input data or output space */
top:
do {
if (bits < 15) {
hold += input[_in++] << bits;
bits += 8;
hold += input[_in++] << bits;
bits += 8;
}
here = lcode[hold & lmask];
dolen:
for (;;) { // Goto emulation
op = here >>> 24/*here.bits*/;
hold >>>= op;
bits -= op;
op = (here >>> 16) & 0xff/*here.op*/;
if (op === 0) { /* literal */
//Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?
// "inflate: literal '%c'\n" :
// "inflate: literal 0x%02x\n", here.val));
output[_out++] = here & 0xffff/*here.val*/;
}
else if (op & 16) { /* length base */
len = here & 0xffff/*here.val*/;
op &= 15; /* number of extra bits */
if (op) {
if (bits < op) {
hold += input[_in++] << bits;
bits += 8;
}
len += hold & ((1 << op) - 1);
hold >>>= op;
bits -= op;
}
//Tracevv((stderr, "inflate: length %u\n", len));
if (bits < 15) {
hold += input[_in++] << bits;
bits += 8;
hold += input[_in++] << bits;
bits += 8;
}
here = dcode[hold & dmask];
dodist:
for (;;) { // goto emulation
op = here >>> 24/*here.bits*/;
hold >>>= op;
bits -= op;
op = (here >>> 16) & 0xff/*here.op*/;
if (op & 16) { /* distance base */
dist = here & 0xffff/*here.val*/;
op &= 15; /* number of extra bits */
if (bits < op) {
hold += input[_in++] << bits;
bits += 8;
if (bits < op) {
hold += input[_in++] << bits;
bits += 8;
}
}
dist += hold & ((1 << op) - 1);
//#ifdef INFLATE_STRICT
if (dist > dmax) {
strm.msg = 'invalid distance too far back';
state.mode = BAD;
break top;
}
//#endif
hold >>>= op;
bits -= op;
//Tracevv((stderr, "inflate: distance %u\n", dist));
op = _out - beg; /* max distance in output */
if (dist > op) { /* see if copy from window */
op = dist - op; /* distance back in window */
if (op > whave) {
if (state.sane) {
strm.msg = 'invalid distance too far back';
state.mode = BAD;
break top;
}
// (!) This block is disabled in zlib defaults,
// don't enable it for binary compatibility
//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
// if (len <= op - whave) {
// do {
// output[_out++] = 0;
// } while (--len);
// continue top;
// }
// len -= op - whave;
// do {
// output[_out++] = 0;
// } while (--op > whave);
// if (op === 0) {
// from = _out - dist;
// do {
// output[_out++] = output[from++];
// } while (--len);
// continue top;
// }
//#endif
}
from = 0; // window index
from_source = s_window;
if (wnext === 0) { /* very common case */
from += wsize - op;
if (op < len) { /* some from window */
len -= op;
do {
output[_out++] = s_window[from++];
} while (--op);
from = _out - dist; /* rest from output */
from_source = output;
}
}
else if (wnext < op) { /* wrap around window */
from += wsize + wnext - op;
op -= wnext;
if (op < len) { /* some from end of window */
len -= op;
do {
output[_out++] = s_window[from++];
} while (--op);
from = 0;
if (wnext < len) { /* some from start of window */
op = wnext;
len -= op;
do {
output[_out++] = s_window[from++];
} while (--op);
from = _out - dist; /* rest from output */
from_source = output;
}
}
}
else { /* contiguous in window */
from += wnext - op;
if (op < len) { /* some from window */
len -= op;
do {
output[_out++] = s_window[from++];
} while (--op);
from = _out - dist; /* rest from output */
from_source = output;
}
}
while (len > 2) {
output[_out++] = from_source[from++];
output[_out++] = from_source[from++];
output[_out++] = from_source[from++];
len -= 3;
}
if (len) {
output[_out++] = from_source[from++];
if (len > 1) {
output[_out++] = from_source[from++];
}
}
}
else {
from = _out - dist; /* copy direct from output */
do { /* minimum length is three */
output[_out++] = output[from++];
output[_out++] = output[from++];
output[_out++] = output[from++];
len -= 3;
} while (len > 2);
if (len) {
output[_out++] = output[from++];
if (len > 1) {
output[_out++] = output[from++];
}
}
}
}
else if ((op & 64) === 0) { /* 2nd level distance code */
here = dcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
continue dodist;
}
else {
strm.msg = 'invalid distance code';
state.mode = BAD;
break top;
}
break; // need to emulate goto via "continue"
}
}
else if ((op & 64) === 0) { /* 2nd level length code */
here = lcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
continue dolen;
}
else if (op & 32) { /* end-of-block */
//Tracevv((stderr, "inflate: end of block\n"));
state.mode = TYPE;
break top;
}
else {
strm.msg = 'invalid literal/length code';
state.mode = BAD;
break top;
}
break; // need to emulate goto via "continue"
}
} while (_in < last && _out < end);
/* return unused bytes (on entry, bits < 8, so in won't go too far back) */
len = bits >> 3;
_in -= len;
bits -= len << 3;
hold &= (1 << bits) - 1;
/* update state and return */
strm.next_in = _in;
strm.next_out = _out;
strm.avail_in = (_in < last ? 5 + (last - _in) : 5 - (_in - last));
strm.avail_out = (_out < end ? 257 + (end - _out) : 257 - (_out - end));
state.hold = hold;
state.bits = bits;
return;
};

File diff suppressed because it is too large Load Diff

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