Compare commits

...

453 commits

Author SHA1 Message Date
Marcos Rodriguez Velez
5f18540ca7 FIX: UX in ManageWallets 2025-03-12 20:42:29 -04:00
GLaDOS
751c7d6f45
Merge pull request #7682 from BlueWallet/menud
REF: MenuItem memory
2025-03-10 18:39:40 +00:00
GLaDOS
0b1c3dd9f7
Merge pull request #7684 from BlueWallet/man
FIX: DIscard changes alert was not working
2025-03-09 19:00:23 +00:00
Marcos Rodriguez Velez
ae89a59794 FIX: DIscard changes alert was not working 2025-03-09 10:23:56 -04:00
Marcos Rodriguez Velez
10b3432e0e OPS: Version bump 2025-03-09 10:17:04 -04:00
Marcos Rodriguez Velez
c67eea8155 REF: Use bottom tabs 2025-03-09 07:45:19 -04:00
Marcos Rodriguez Velez
9421511f74 Update useMenuElements.ts 2025-03-08 19:11:51 -04:00
Marcos Rodriguez Velez
9ec0ef51e4 Update useMenuElements.ios.ts 2025-03-08 19:09:41 -04:00
Marcos Rodriguez Velez
1cada11c50 REF: MenuItem memory 2025-03-08 11:06:55 -04:00
Marcos Rodriguez Velez
d2cebde6ad OPS: Version bump 2025-03-08 09:38:46 -04:00
GLaDOS
1a940971bc
Merge pull request #7677 from BlueWallet/scanln
REF: ScanLNDInvoice to TSX
2025-03-08 09:22:24 +00:00
GLaDOS
28316b4d73
Merge pull request #7654 from BlueWallet/renovate/androidx.constraintlayout-constraintlayout-2.x
Update dependency androidx.constraintlayout:constraintlayout to v2.2.1
2025-03-08 08:35:37 +00:00
GLaDOS
4670eea38a
Merge pull request #7681 from BlueWallet/ele
FIX: Can not turn Electrum Server on. #7680
2025-03-08 08:15:05 +00:00
Marcos Rodriguez Vélez
b2552bdc71
Merge branch 'master' into ele 2025-03-07 22:29:36 -04:00
Marcos Rodriguez Vélez
dbd4066f7e
Merge branch 'master' into scanln 2025-03-07 22:29:25 -04:00
Marcos Rodriguez Vélez
4cdd952f90
FIX: Update use of CoinDesk API (#7678) 2025-03-07 22:29:12 -04:00
Marcos Rodriguez Velez
ddee4cdaaf FIX: Can not turn Electrum Server on. #7680 2025-03-07 21:58:33 -04:00
Marcos Rodriguez Velez
0aa6b96e4b w 2025-03-07 19:07:15 -04:00
Marcos Rodriguez Velez
8d49aff279 Merge branch 'master' into scanln 2025-03-07 19:03:43 -04:00
GLaDOS
18a187b120
Merge pull request #7663 from BlueWallet/virw
REF: View Edit Multisig navigation
2025-03-06 10:17:44 +00:00
Marcos Rodriguez Vélez
1f77a852a8
Rename LazyLoadScanLndInvoiceStack.tsx to LazyLoadScanLNDInvoiceStack.tsx 2025-03-05 23:12:45 -04:00
Marcos Rodriguez Vélez
9d899d672d
Rename ScanLndInvoiceStack.tsx to ScanLNDInvoiceStack.tsx 2025-03-05 23:09:28 -04:00
Marcos Rodriguez Velez
e7b81e5517 wi 2025-03-05 21:50:50 -04:00
Marcos Rodriguez Velez
f8af06e2ae REF: ScanLNDInvoice to TSX 2025-03-05 21:43:02 -04:00
Marcos Rodriguez Velez
8b81472fa4 OPS: Master was broken for catalyst 2025-03-05 20:54:50 -04:00
Marcos Rodriguez Velez
4ad2b15070 OPS: Lock file 2025-03-05 20:34:29 -04:00
Marcos Rodriguez Velez
dd118af993 Update project.pbxproj 2025-03-05 20:31:25 -04:00
GLaDOS
d375bd9780
Merge pull request #7672 from BlueWallet/cc
REF: CompanionDelegate to hook
2025-03-05 22:54:21 +00:00
GLaDOS
c967f6701a
Merge pull request #7675 from BlueWallet/revert-7631-headr
Revert "REF: Wallet tranaction header animation"
2025-03-04 11:13:29 +00:00
Overtorment
4753d984ca
Revert "REF: Wallet tranaction header animation" 2025-03-04 10:16:58 +00:00
GLaDOS
28e2e343b8
Merge pull request #7631 from BlueWallet/headr
REF: Wallet tranaction header animation
2025-03-03 19:53:08 +00:00
Marcos Rodriguez Velez
b81879e9bd Update WalletTransactions.tsx 2025-03-03 14:59:49 -04:00
Marcos Rodriguez Velez
040f91028a Update ScanQRCode.tsx 2025-03-03 14:33:49 -04:00
Marcos Rodriguez Velez
1c8aa08de8 Merge branch 'master' into cc 2025-03-03 14:22:21 -04:00
Marcos Rodriguez Velez
d7743a740f Merge branch 'master' into virw 2025-03-03 14:00:14 -04:00
Marcos Rodriguez Velez
a2ec407720 Merge branch 'master' into headr 2025-03-03 13:43:36 -04:00
GLaDOS
c90bf68a66
Merge pull request #7643 from BlueWallet/scr
FIX: Disable screen protect until it supports nav 7
2025-03-03 17:10:05 +00:00
Marcos Rodriguez Velez
7ab73669dc wip 2025-03-03 12:06:53 -04:00
Marcos Rodriguez Velez
72134fef84 Merge branch 'master' into scr 2025-03-03 12:05:49 -04:00
GLaDOS
c0eec1be8e
Merge pull request #7674 from BlueWallet/marcosrdz-patch-4
Update AndroidManifest.xml
2025-03-03 14:41:36 +00:00
Marcos Rodriguez Vélez
77f9cf7e16
Update AndroidManifest.xml 2025-03-03 09:39:13 -04:00
renovate[bot]
88be0332e4
Update dependency androidx.constraintlayout:constraintlayout to v2.2.1 2025-03-03 13:12:51 +00:00
GLaDOS
a4f8e42d8c
Merge pull request #7673 from BlueWallet/pa
OPS: Packages updates
2025-03-03 13:11:48 +00:00
Marcos Rodriguez Vélez
e519360d89
Update bluewallet2.spec.js 2025-03-03 04:25:38 -04:00
Marcos Rodriguez Velez
62a5efc82c OPS: Packages updates 2025-03-03 03:39:00 -04:00
Marcos Rodriguez Velez
ec2bc5e627 wip 2025-03-03 03:30:19 -04:00
Marcos Rodriguez Velez
0bdfc6fa85 Update WalletTransactions.tsx 2025-03-03 01:17:43 -04:00
Marcos Rodriguez Velez
ef5887f28b REF: CompanionDelegate to hook 2025-03-02 22:12:50 -04:00
Marcos Rodriguez Velez
3f82cb4449 Update WalletTransactions.tsx 2025-03-02 21:46:28 -04:00
Marcos Rodriguez Velez
9574554780 Update WalletDetails.tsx 2025-03-02 20:43:19 -04:00
Marcos Rodriguez Velez
4b249edaa4 Update WalletDetails.tsx 2025-03-02 18:43:40 -04:00
Marcos Rodriguez Velez
e3fcbbb713 Revert "Update WalletDetails.tsx"
This reverts commit 2376ef8be9.
2025-03-02 18:23:00 -04:00
Marcos Rodriguez Velez
63a3c61534 Revert "Update WalletDetails.tsx"
This reverts commit a6d66574cd.
2025-03-02 18:22:58 -04:00
Marcos Rodriguez Velez
a6d66574cd Update WalletDetails.tsx 2025-03-02 18:03:17 -04:00
Marcos Rodriguez Velez
2376ef8be9 Update WalletDetails.tsx 2025-03-02 18:03:07 -04:00
Marcos Rodriguez Vélez
155f021692
Merge branch 'master' into headr 2025-03-02 15:54:50 -04:00
GLaDOS
3a26d6dab2
Merge pull request #7667 from BlueWallet/yml
OPS: Refactor iOS pipeline
2025-03-02 19:49:19 +00:00
Marcos Rodriguez Velez
1da481542a Update build-ios-release-pullrequest.yml 2025-03-02 12:57:34 -04:00
Marcos Rodriguez Velez
472307c271 Revert "Update Fastfile"
This reverts commit 09394ff4f9.
2025-03-02 12:47:13 -04:00
Marcos Rodriguez Velez
09394ff4f9 Update Fastfile 2025-03-02 12:37:30 -04:00
Marcos Rodriguez Velez
16936fca27 wip 2025-03-02 12:20:42 -04:00
Marcos Rodriguez Velez
54166c0592 Update Fastfile 2025-03-02 11:54:38 -04:00
Marcos Rodriguez Velez
bb6d443670 wwip 2025-03-02 11:36:33 -04:00
Marcos Rodriguez Velez
e4e16a8f40 Update Fastfile 2025-03-02 05:16:02 -04:00
Marcos Rodriguez Velez
35deca58e0 Update build-ios-release-pullrequest.yml 2025-03-02 04:52:48 -04:00
Marcos Rodriguez Velez
898443f3a5 Update TransactionsNavigationHeader.tsx 2025-03-02 04:51:07 -04:00
Marcos Rodriguez Velez
15fc708a0a Update Fastfile 2025-03-02 04:47:37 -04:00
Marcos Rodriguez Velez
15c618b59a Update Gemfile.lock 2025-03-02 02:01:12 -04:00
Marcos Rodriguez Velez
ccdb492ba0 w 2025-03-02 01:49:30 -04:00
Marcos Rodriguez Velez
0449965ef5 Revert "wip"
This reverts commit 863ac46bc8.
2025-03-02 01:11:36 -04:00
Marcos Rodriguez Velez
758c2acf3a Revert "Update Fastfile"
This reverts commit aa695f2705.
2025-03-02 01:11:33 -04:00
Marcos Rodriguez Velez
b75aa7b269 wip 2025-03-02 01:11:24 -04:00
Marcos Rodriguez Velez
aa695f2705 Update Fastfile 2025-03-02 01:09:40 -04:00
Marcos Rodriguez Velez
863ac46bc8 wip 2025-03-02 00:02:05 -04:00
Marcos Rodriguez Velez
1946fa0dde OPS: Refactor iOS pipeline 2025-03-01 23:51:54 -04:00
Marcos Rodriguez Velez
a62a21b28b Update WalletTransactions.tsx 2025-03-01 23:32:18 -04:00
Marcos Rodriguez Velez
c1ae300254 Update WalletTransactions.tsx 2025-03-01 22:09:53 -04:00
Marcos Rodriguez Velez
5e4d58b207 Update WalletTransactions.tsx 2025-03-01 21:40:46 -04:00
Marcos Rodriguez Velez
3504d0dc30 Revert "Update WalletTransactions.tsx"
This reverts commit 05491387ff.
2025-03-01 21:24:15 -04:00
Marcos Rodriguez Velez
fe795e648b Revert "Update WalletTransactions.tsx"
This reverts commit a65776933d.
2025-03-01 21:24:12 -04:00
Marcos Rodriguez Velez
af8d7d3477 Revert "Update WalletTransactions.tsx"
This reverts commit 88b8274758.
2025-03-01 21:24:09 -04:00
Marcos Rodriguez Velez
c604ac4197 Merge branch 'master' into headr 2025-03-01 21:23:17 -04:00
Marcos Rodriguez Vélez
4c0fd89530
Update Fastfile 2025-03-01 19:57:55 -04:00
Marcos Rodriguez Vélez
d14b4265f8
Update build-ios-release-pullrequest.yml 2025-03-01 15:41:07 -04:00
Marcos Rodriguez Velez
307e950d15 OPS: Downgrade fastlane 2025-03-01 15:30:21 -04:00
Marcos Rodriguez Velez
10f145d012 wip 2025-03-01 15:21:18 -04:00
Marcos Rodriguez Velez
88b8274758 Update WalletTransactions.tsx 2025-03-01 15:06:57 -04:00
Marcos Rodriguez Velez
a65776933d Update WalletTransactions.tsx 2025-03-01 15:06:47 -04:00
Marcos Rodriguez Velez
05491387ff Update WalletTransactions.tsx 2025-03-01 14:36:42 -04:00
Marcos Rodriguez Velez
e23f233f25 Merge branch 'master' into headr 2025-03-01 14:24:50 -04:00
Marcos Rodriguez Vélez
7eb420c561
Update build-ios-release-pullrequest.yml 2025-03-01 13:41:18 -04:00
Marcos Rodriguez Velez
96e553f3d5 Update WalletTransactions.tsx 2025-03-01 13:21:50 -04:00
Marcos Rodriguez Velez
2f3cf1b4e9 Update WalletTransactions.tsx 2025-03-01 12:14:52 -04:00
Marcos Rodriguez Velez
6a4392de02 Merge branch 'master' into headr 2025-03-01 12:09:49 -04:00
GLaDOS
62bb33a9ff
Merge pull request #7665 from BlueWallet/androidc
FIX: Android cold open crash
2025-03-01 15:40:15 +00:00
GLaDOS
85cd7b4aed
Merge pull request #7664 from BlueWallet/menuview
DEL: ios-context package
2025-03-01 15:08:37 +00:00
Marcos Rodriguez Velez
9507a48314 Update BitcoinPriceWidget.kt 2025-03-01 10:44:15 -04:00
Marcos Rodriguez Velez
07b93d521d FIX: Android cold open crash 2025-03-01 10:38:33 -04:00
Marcos Rodriguez Velez
7c6bf01372 Merge branch 'menuview' into headr 2025-03-01 10:32:29 -04:00
Marcos Rodriguez Velez
b01aa58e3b DEL: ios-context package
RNMenu covers 95% of use case
2025-03-01 10:17:56 -04:00
Marcos Rodriguez Velez
136dd20f9e REF: View Edit Multisig navigation
Easier to popTo since its just 1  screen
2025-03-01 10:10:00 -04:00
Marcos Rodriguez Velez
0bfeda0d75 Merge branch 'master' into headr 2025-03-01 10:05:23 -04:00
Marcos Rodriguez Velez
1a848328e3 Update project.pbxproj 2025-03-01 10:01:59 -04:00
Marcos Rodriguez Vélez
93e6269611
Update build-ios-release-pullrequest.yml 2025-02-28 21:47:31 -04:00
Marcos Rodriguez Vélez
5f8dbc52d1
Update build-ios-release-pullrequest.yml 2025-02-28 21:45:54 -04:00
Marcos Rodriguez Vélez
e4d3ecba98
Update build-ios-release-pullrequest.yml 2025-02-28 21:42:58 -04:00
Marcos Rodriguez Velez
bbe4449dd9 Update WalletTransactions.tsx 2025-02-28 21:27:30 -04:00
Marcos Rodriguez Velez
bf9087eae6 Merge branch 'master' into headr 2025-02-28 21:16:51 -04:00
GLaDOS
b3ff1b7c3f
Merge pull request #7661 from BlueWallet/menu
FIX: MenuElements for macOS and iPad were not firing on nav 7
2025-02-28 22:49:39 +00:00
Marcos Rodriguez Velez
00dcc25142 wip 2025-02-28 18:19:33 -04:00
GLaDOS
4614c51041
Merge pull request #7657 from BlueWallet/locsync20
fix: sync language files
2025-02-28 22:07:39 +00:00
Marcos Rodriguez Velez
8d694ceb7b Merge branch 'master' into headr 2025-02-28 18:00:59 -04:00
Marcos Rodriguez Velez
49f6068b21 FIX: MenuElements for macOS and iPad were not firing on nav 7 2025-02-28 17:57:10 -04:00
Ivan Vershigora
be8437e107 fix: sync language files 2025-02-28 13:59:06 +00:00
GLaDOS
cc71dfce8c
Merge pull request #7658 from BlueWallet/targets
fix: on SendDetails screen move targets creation out of options loop
2025-02-28 13:03:03 +00:00
GLaDOS
2e1f20c080
Merge pull request #7659 from BlueWallet/send-deps
fix: remove eslint-ignore on SendDetails screen
2025-02-28 13:03:00 +00:00
Ivan Vershigora
11dceb19fa
fix: remove eslint-ignore on SendDetails screen 2025-02-28 12:15:40 +00:00
Ivan Vershigora
7bb3dd6aef
fix: on SendDetails screen move targets creation out of options loop 2025-02-28 11:52:06 +00:00
GLaDOS
7e7492d314
Merge pull request #7656 from BlueWallet/fix-7653
FIX: crash when scanning invoice with both ln and onchain address (cl…
2025-02-28 11:33:16 +00:00
GLaDOS
c4b1e67f9d
Merge pull request #7648 from BlueWallet/fix-tx-list-screen-update
FIX: transactions list screen would not always update with new transactions
2025-02-28 11:14:54 +00:00
GLaDOS
79f624e906
Merge pull request #7652 from BlueWallet/ref-android-startup-time
REF: improve startup time
2025-02-28 11:14:51 +00:00
overtorment
fabfc5c156 FIX: crash when scanning invoice with both ln and onchain address (closes #7653) 2025-02-28 10:38:23 +00:00
Overtorment
a3d234bee1
Update build.gradle 2025-02-28 09:49:35 +00:00
Marcos Rodriguez Velez
850ac2c653 Merge branch 'master' into scr 2025-02-27 23:25:26 -04:00
GLaDOS
1c26cb420e
Merge pull request #7651 from BlueWallet/scanmac
FIX: Restore camera access to mac app
2025-02-28 01:09:08 +00:00
Marcos Rodriguez Velez
0f23c4d0a7 Merge branch 'master' into headr 2025-02-27 20:10:41 -04:00
Marcos Rodriguez Velez
9adef2b3c1 Update CameraScreen.tsx 2025-02-27 20:10:01 -04:00
Marcos Rodriguez Velez
a9b003e762 FIX:pacakge hash 2025-02-27 20:06:34 -04:00
Marcos Rodriguez Velez
37b03b12e7 Update CameraScreen.tsx 2025-02-27 19:28:35 -04:00
Marcos Rodriguez Velez
4e7c5a28ae Merge branch 'master' into scr 2025-02-27 15:31:39 -04:00
Marcos Rodriguez Velez
9766d2387a Merge branch 'master' into scanmac 2025-02-27 15:30:24 -04:00
GLaDOS
3939ef32f9
Merge pull request #7644 from BlueWallet/renovate/react-native-community-cli-15.x
Update dependency @react-native-community/cli to v15.1.3
2025-02-27 19:20:48 +00:00
Marcos Rodriguez Vélez
17a5a78fd8
Merge branch 'master' into scanmac 2025-02-27 15:10:45 -04:00
overtorment
f7d673d93b REF: improve startup time 2025-02-27 19:06:39 +00:00
GLaDOS
dc3e88c005
Merge pull request #7649 from BlueWallet/translations_loc-en-json--master_es_419
Updates for file loc/en.json in es_419
2025-02-27 18:41:41 +00:00
overtorment
85cb6c1287 FIX: transactions list screen would not always update with new transactions 2025-02-27 18:41:29 +00:00
Marcos Rodriguez Velez
8488dfb9e7 wip 2025-02-27 14:15:40 -04:00
Marcos Rodriguez Velez
3dde81f3a8 Update CameraScreen.tsx 2025-02-27 14:03:42 -04:00
Marcos Rodriguez Velez
a0dc0a31e7 FIX: Restore camera access to mac app 2025-02-27 13:18:41 -04:00
GLaDOS
2dc26ac26a
Merge pull request #7650 from BlueWallet/fix-ln-refill-address
FIX: when pressing refill on lightning wallet, address would not subs…
2025-02-27 16:44:13 +00:00
overtorment
b881370f83 FIX: when pressing refill on lightning wallet, address would not subsitute 2025-02-27 15:54:48 +00:00
transifex-integration[bot]
4f31aff503
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2025-02-27 14:03:45 +00:00
Marcos Rodriguez Velez
2ee13dcf4f Merge branch 'master' into headr 2025-02-26 00:48:33 -04:00
Marcos Rodriguez Velez
bf65d2d302 Merge branch 'master' into scr 2025-02-26 00:27:54 -04:00
renovate[bot]
3599ef50ad
Update dependency @react-native-community/cli to v15.1.3 2025-02-26 03:18:23 +00:00
GLaDOS
80fb4a74a8
Merge pull request #7641 from BlueWallet/renovate/react-native-screens-4.x
Update dependency react-native-screens to v4.9.1
2025-02-26 03:11:45 +00:00
Marcos Rodriguez Vélez
4f326452ab
Merge branch 'master' into renovate/react-native-screens-4.x 2025-02-25 22:26:36 -04:00
Marcos Rodriguez Velez
37a88fd60d Update ImportWallet.tsx 2025-02-25 22:24:33 -04:00
Marcos Rodriguez Velez
3fcf3c0840 FIX: Disable screen protect until it supports nav 7. Fix speed issue exposed by disabling it 2025-02-25 22:24:31 -04:00
GLaDOS
685332ce22
Merge pull request #7636 from BlueWallet/cache
OPS: Remove all cache when building from master.
2025-02-25 20:08:13 +00:00
Marcos Rodriguez Velez
0892332d23 OPS: Version bump 2025-02-25 14:43:15 -04:00
Marcos Rodriguez Velez
11cb7dbed5 Update Podfile.lock 2025-02-25 14:34:27 -04:00
GLaDOS
7205f70c30
Merge pull request #7640 from BlueWallet/toralert
FIX: Attempts to connect without Orbot would not indicate errors
2025-02-25 17:01:10 +00:00
renovate[bot]
aa9e647c28
Update dependency react-native-screens to v4.9.1 2025-02-25 16:54:06 +00:00
GLaDOS
8439ff9893
Merge pull request #7639 from BlueWallet/renovate/lodev09-react-native-true-sheet-digest
Update @lodev09/react-native-true-sheet digest to 5945184
2025-02-25 16:43:37 +00:00
GLaDOS
f83476428e
Merge pull request #7638 from BlueWallet/fix-request-camera-permish
FIX: request camera permission
2025-02-25 16:31:30 +00:00
Marcos Rodriguez Velez
4be2668c81 FIX: Attempts to connect without Orbot would not indicate errors 2025-02-25 11:39:56 -04:00
renovate[bot]
867581003c
Update @lodev09/react-native-true-sheet digest to 5945184 2025-02-25 15:39:31 +00:00
GLaDOS
3f1ea9432b
Merge pull request #7633 from BlueWallet/elecf
FIX: Dont disable save button by isLoading as it can mean client is n…
2025-02-25 15:26:28 +00:00
GLaDOS
2bcbe9903e
Merge pull request #7635 from BlueWallet/renovate/react-native-svg-15.x
Update dependency react-native-svg to v15.11.2
2025-02-25 15:26:23 +00:00
overtorment
7963083237 FIX: request camera permission 2025-02-25 15:24:43 +00:00
GLaDOS
b7e03c1ed1
Merge pull request #7629 from BlueWallet/wrongonpres
FIX: Wrongf onpress
2025-02-25 14:43:47 +00:00
GLaDOS
b74fb5f389
Merge pull request #7630 from BlueWallet/macw
REF: Reuse existing component
2025-02-25 14:22:05 +00:00
Marcos Rodriguez Velez
ddd141fc11 Update package.json 2025-02-25 09:41:46 -04:00
GLaDOS
d9a70f5879
Merge pull request #7608 from BlueWallet/Carousel-on-the-main-screen-does-not-working-from-the-first-go
FIX: Carousel on the main screen does not working from the first go
2025-02-25 10:20:40 +00:00
Marcos Rodriguez Velez
e645c911d7 OPS: Version bump 2025-02-24 21:37:56 -04:00
Marcos Rodriguez Velez
06fbb8c945 OPS: Remove all cache when building from master. 2025-02-24 21:36:20 -04:00
Marcos Rodriguez Velez
c008a644cc Merge branch 'master' into Carousel-on-the-main-screen-does-not-working-from-the-first-go 2025-02-24 21:34:56 -04:00
renovate[bot]
ab7e7cf1d5
Update dependency react-native-svg to v15.11.2 2025-02-24 23:19:56 +00:00
GLaDOS
d38968086e
Merge pull request #7626 from BlueWallet/listener
FIX: iOS runtime crash & update screens package
2025-02-24 13:13:56 +00:00
GLaDOS
787fcb797d
Merge pull request #7628 from BlueWallet/refactor-bring-back-scan-qr-helper
refactor: bring back scan qr helper
2025-02-24 10:17:26 +00:00
Marcos Rodriguez Velez
599b5d3b60 REF: Use new helpers for animation. RemoveonPressout 2025-02-24 02:41:00 -04:00
Marcos Rodriguez Velez
c3f7d5b184 Merge branch 'master' into Carousel-on-the-main-screen-does-not-working-from-the-first-go 2025-02-24 02:40:03 -04:00
Marcos Rodriguez Velez
3423730a41 Update WalletsCarousel.tsx 2025-02-24 02:39:51 -04:00
Marcos Rodriguez Velez
2bb7b0c53f REF: Wallet carousel item animation to use new helperds 2025-02-24 02:39:28 -04:00
Marcos Rodriguez Velez
b0a7053fc0 Merge branch 'master' into headr 2025-02-24 02:21:59 -04:00
GLaDOS
a8c0da4768
Merge pull request #7632 from BlueWallet/renovate/react-native-device-info-14.x
Update dependency react-native-device-info to v14.0.4
2025-02-24 05:04:33 +00:00
Marcos Rodriguez Velez
b357053e65 FIX: Dont disable save button by isLoading as it can mean client is not connected 2025-02-24 00:54:40 -04:00
Marcos Rodriguez Velez
5b20ac352e Update WalletTransactions.tsx 2025-02-24 00:26:15 -04:00
renovate[bot]
8c2640e5a9
Update dependency react-native-device-info to v14.0.4 2025-02-24 03:52:24 +00:00
Marcos Rodriguez Velez
a6575b7b73 Merge branch 'master' into macw 2025-02-23 23:02:37 -04:00
Marcos Rodriguez Velez
db58bcf70d Update ImportWallet.tsx 2025-02-23 23:02:34 -04:00
Marcos Rodriguez Velez
b8d1d686f0 Merge branch 'master' into headr 2025-02-23 23:00:03 -04:00
Marcos Rodriguez Velez
5d088a67c1 REF: Wallet tranaction header animation 2025-02-23 22:59:22 -04:00
GLaDOS
fa43d03a36
Merge pull request #7627 from BlueWallet/translations_loc-en-json--master_pl
Updates for file loc/en.json in pl
2025-02-24 01:39:35 +00:00
Marcos Rodriguez Velez
5a7fb86742 Update addMultisigStep2.js 2025-02-23 21:16:38 -04:00
Marcos Rodriguez Velez
4ff759b538 wip 2025-02-23 21:15:10 -04:00
Marcos Rodriguez Velez
0b33af59f0 Update TooltipMenu.tsx 2025-02-23 21:05:45 -04:00
Marcos Rodriguez Velez
f9d8594509 wip 2025-02-23 20:53:46 -04:00
Marcos Rodriguez Velez
7f97c340f8 REF: Reuse existing component 2025-02-23 18:18:12 -04:00
Marcos Rodriguez Velez
925dc17042 FIX: Wrongf onpress 2025-02-23 17:35:19 -04:00
overtorment
164f16657a refactor: bring back scan qr helper 2025-02-23 21:09:14 +00:00
overtorment
34db010bde REF: scanQr to TS 2025-02-23 18:57:25 +00:00
transifex-integration[bot]
7ccf19212f
Translate loc/en.json in pl
100% reviewed source file: 'loc/en.json'
on 'pl'.
2025-02-23 15:32:37 +00:00
Marcos Rodriguez Velez
133312e065 FIX: iOS runtime crash & update screens package
Lower the file changes in Receive modal PR
2025-02-23 08:31:55 -04:00
Marcos Rodriguez Vélez
46a78e8dfb
Update AppDelegate.mm 2025-02-22 17:32:14 -04:00
GLaDOS
e5ab5f6565
Merge pull request #7566 from BlueWallet/detailsitem
ADD: TX Details action action
2025-02-22 20:11:05 +00:00
GLaDOS
ad71dccd72
Merge pull request #7620 from BlueWallet/usede
REF: use debounce in wallet transactions to avoid rapid reattempts
2025-02-22 17:28:54 +00:00
Marcos Rodriguez Velez
159a8b2e16 Merge branch 'master' into usede 2025-02-22 12:40:23 -04:00
Marcos Rodriguez Velez
d3fd8c050f Update WalletTransactions.tsx 2025-02-22 12:40:17 -04:00
GLaDOS
52b3cb9b34
Merge pull request #7622 from BlueWallet/swift
REF: ObjC emitters to Swift
2025-02-22 15:53:52 +00:00
Marcos Rodriguez Velez
98b643a023 Update WalletTransactions.tsx 2025-02-22 11:49:53 -04:00
Marcos Rodriguez Velez
0eb3393f1f wip 2025-02-22 09:38:12 -04:00
Marcos Rodriguez Velez
47a08448a2 REF: ObjC emitters to Swift 2025-02-22 09:16:25 -04:00
Marcos Rodriguez Velez
254550d92e Update CommonToolTipActions.ts 2025-02-22 09:00:56 -04:00
Marcos Rodriguez Velez
9ed7ceabdb Update WalletTransactions.tsx 2025-02-22 08:37:46 -04:00
Marcos Rodriguez Velez
6afc6624eb wip 2025-02-21 23:14:00 -04:00
Marcos Rodriguez Velez
2317d0a4cf Update WalletTransactions.tsx 2025-02-21 22:05:28 -04:00
Marcos Rodriguez Velez
dcd2023815 REF: use debounce in wallet transactions to avoid rapid reattempts 2025-02-21 21:57:37 -04:00
Marcos Rodriguez Vélez
4b37eaba98
Merge branch 'master' into detailsitem 2025-02-21 21:42:56 -04:00
Marcos Rodriguez Velez
8619e80dc0 OPS: Move nav devtools to packages 2025-02-21 18:20:15 +00:00
GLaDOS
e06d1ce57c
Merge pull request #7614 from BlueWallet/PSBT---Saved-at-a-not-accessible-location-on-Android-13-#7600
FIX: PSBT - Saved at a not accessible location on Android 13 #7600
2025-02-21 17:09:44 +00:00
Overtorment
157bd3529a
Merge branch 'master' into detailsitem 2025-02-21 16:56:49 +00:00
GLaDOS
e92eb7eae0
Merge pull request #7564 from BlueWallet/contr
FIX: Lndhub lacked timeouts
2025-02-21 16:50:53 +00:00
GLaDOS
b9227cdbc6
Merge pull request #7613 from BlueWallet/renovate/react-native-draglist-digest
Update react-native-draglist digest to 8c52785
2025-02-21 02:50:19 +00:00
Marcos Rodriguez Velez
a047c0219e FIX: PSBT - Saved at a not accessible location on Android 13 #7600 2025-02-20 20:59:30 -04:00
renovate[bot]
2ca8eca810
Update react-native-draglist digest to 8c52785 2025-02-21 00:52:43 +00:00
GLaDOS
93f901e94f
Merge pull request #7612 from BlueWallet/renovate/react-native-menu-menu-digest
Update @react-native-menu/menu digest to 038a9c9
2025-02-21 00:43:24 +00:00
Marcos Rodriguez Vélez
670ad6a833
Merge branch 'master' into renovate/react-native-menu-menu-digest 2025-02-20 19:34:58 -04:00
Marcos Rodriguez Vélez
b232a13243
Merge branch 'master' into contr 2025-02-20 19:33:32 -04:00
Marcos Rodriguez Velez
d610063809 Update TransactionListItem.tsx 2025-02-20 19:32:22 -04:00
Marcos Rodriguez Velez
1b328cd130 Merge branch 'master' into detailsitem 2025-02-20 19:32:03 -04:00
Marcos Rodriguez Velez
0b5e640630 Revert "ADD: TX Details action action"
This reverts commit cdd923db7c.
2025-02-20 19:32:01 -04:00
Marcos Rodriguez Velez
527219f697 Update lndHub.ts 2025-02-20 19:30:03 -04:00
GLaDOS
c400771d7a
Merge pull request #7609 from BlueWallet/erro
FIX: Error on send would not scroll to recipient
2025-02-20 23:29:31 +00:00
Marcos Rodriguez Velez
021ed454f1 Merge branch 'master' into contr 2025-02-20 19:29:27 -04:00
GLaDOS
47673a4ae0
Merge pull request #7604 from BlueWallet/addressfixes
Addressfixes
2025-02-20 23:07:08 +00:00
renovate[bot]
7e66e42862
Update @react-native-menu/menu digest to 038a9c9 2025-02-20 23:06:53 +00:00
GLaDOS
e25833f0d3
Merge pull request #7610 from BlueWallet/renovate/lodev09-react-native-true-sheet-digest
Update @lodev09/react-native-true-sheet digest to 0fefdd1
2025-02-20 22:56:00 +00:00
Marcos Rodriguez Velez
0e698069f4 Merge branch 'master' into Carousel-on-the-main-screen-does-not-working-from-the-first-go 2025-02-20 18:32:10 -04:00
Marcos Rodriguez Velez
3a8a7d6da8 Update en.json 2025-02-20 18:09:54 -04:00
Marcos Rodriguez Velez
1b561c8a91 Update lightning-custodian-wallet.ts 2025-02-20 18:09:28 -04:00
Marcos Rodriguez Velez
4b93827b7f Merge branch 'master' into contr 2025-02-20 18:06:15 -04:00
Marcos Rodriguez Velez
3de6976997 Merge branch 'master' into addressfixes 2025-02-20 18:05:33 -04:00
Marcos Rodriguez Velez
9c1be484c1 Update BottomModal.tsx 2025-02-20 18:03:30 -04:00
Marcos Rodriguez Velez
f04b50c58b Merge branch 'master' into renovate/lodev09-react-native-true-sheet-digest 2025-02-20 18:03:22 -04:00
GLaDOS
f974658472
Merge pull request #7596 from BlueWallet/cleanup
FIX: Slight cleanup
2025-02-20 18:37:10 +00:00
Marcos Rodriguez Vélez
208157430f
Merge branch 'master' into erro 2025-02-20 11:32:26 -04:00
Marcos Rodriguez Vélez
b73f04b4e6
Merge branch 'master' into renovate/lodev09-react-native-true-sheet-digest 2025-02-20 11:10:56 -04:00
Marcos Rodriguez Vélez
0429721d66
FIX: Receive close button padding (#7602) 2025-02-20 11:10:39 -04:00
Marcos Rodriguez Vélez
5fb3991cb2
Merge branch 'master' into renovate/lodev09-react-native-true-sheet-digest 2025-02-20 08:45:42 -04:00
GLaDOS
e4093a357d
Merge pull request #7606 from BlueWallet/screens
FIX: UI for scan qr code was not full height when reejcted
2025-02-20 12:29:20 +00:00
renovate[bot]
fda596211a
Update @lodev09/react-native-true-sheet digest to 0fefdd1 2025-02-19 19:32:38 +00:00
Marcos Rodriguez Velez
fb1a30191d Update SendDetails.tsx 2025-02-19 09:57:59 -04:00
Marcos Rodriguez Velez
2f3ac6e972 wip 2025-02-19 09:20:27 -04:00
Marcos Rodriguez Velez
8719ded414 wip 2025-02-19 09:11:01 -04:00
Marcos Rodriguez Velez
680d9d4495 Merge branch 'master' into erro 2025-02-19 09:03:23 -04:00
Marcos Rodriguez Velez
45f095badf REF: Refactor helper 2025-02-19 09:00:49 -04:00
Marcos Rodriguez Velez
66bb0b0e1c Merge branch 'master' into Carousel-on-the-main-screen-does-not-working-from-the-first-go 2025-02-19 08:55:55 -04:00
GLaDOS
33acf30d68
Merge pull request #7605 from BlueWallet/scand
OPS: Move rn devtools to devdep
2025-02-19 11:15:19 +00:00
GLaDOS
2c4bb95475
Merge pull request #7607 from BlueWallet/addw
FIX:  Close button in Add
2025-02-19 11:15:14 +00:00
Ivan
9435fb769f
Merge branch 'master' into addressfixes 2025-02-19 11:04:08 +00:00
GLaDOS
6321627578
Merge pull request #7603 from BlueWallet/receivescreenswit
FIX: unblock UI as user switches address type
2025-02-19 11:00:14 +00:00
Marcos Rodriguez Velez
18cb2faef6 Update SendDetails.tsx 2025-02-18 22:53:43 -04:00
Marcos Rodriguez Velez
2c68583495 FIX: Error on send would not scroll to recipient 2025-02-18 22:51:17 -04:00
Marcos Rodriguez Velez
f8629e2555 FIX: Carousel on the main screen does not working from the first go 2025-02-18 22:44:02 -04:00
Marcos Rodriguez Velez
6c11e2a5b8 FIX: Close button in Add 2025-02-18 22:34:19 -04:00
Marcos Rodriguez Velez
827c2ad3db FIX: UI for scan qr code was not full height when reejcted 2025-02-18 22:26:51 -04:00
Marcos Rodriguez Velez
4be2bb03be OPS: Move rn devtools to devdep 2025-02-18 22:00:44 -04:00
Marcos Rodriguez Velez
12d8596180 Update WalletAddresses.tsx 2025-02-18 21:55:57 -04:00
Marcos Rodriguez Velez
2a4b14d63e Update AddressItem.tsx 2025-02-18 21:51:01 -04:00
Marcos Rodriguez Velez
062b8844d4 FIX: Address could get fully converted to "..." in some scenarios 2025-02-18 21:36:38 -04:00
Marcos Rodriguez Velez
e4cea4f451 Revert "FIX: unblock UI as user switches address type"
This reverts commit a1d5941a75.
2025-02-18 21:25:03 -04:00
Marcos Rodriguez Velez
82f13fbded Merge branch 'nav7frixes' into receivescreenswit 2025-02-18 13:59:31 -04:00
Marcos Rodriguez Velez
a1d5941a75 FIX: unblock UI as user switches address type 2025-02-18 13:58:55 -04:00
Marcos Rodriguez Velez
4d9a2f79f9 Update WalletsCarousel.tsx 2025-02-18 13:42:18 -04:00
Marcos Rodriguez Velez
fef62f2fd8 Merge branch 'master' into cleanup 2025-02-18 13:38:30 -04:00
Marcos Rodriguez Velez
c1adabb021 REF: Error messasge 2025-02-18 13:36:07 -04:00
Marcos Rodriguez Velez
b42290ceee REF: Rename util 2025-02-18 13:27:35 -04:00
Marcos Rodriguez Velez
0aa2ed20f3 Merge branch 'master' into contr 2025-02-18 13:18:14 -04:00
Marcos Rodriguez Velez
c092ea4523 FIX: Receive close button padding 2025-02-18 13:15:07 -04:00
Marcos Rodriguez Vélez
d338f813cb
OPS: Upgrade RNav 7 (#7419) 2025-02-17 15:24:05 -04:00
Marcos Rodriguez Velez
238ee798ab Update lnurl.ts 2025-02-16 22:26:34 -04:00
Marcos Rodriguez Velez
bbf746b011 REF: fetch to timeoutFetch 2025-02-16 22:26:03 -04:00
Marcos Rodriguez Velez
fdd2b66d8e Merge branch 'master' into contr 2025-02-16 22:23:47 -04:00
Marcos Rodriguez Velez
44fc028159 Create fetch.ts 2025-02-16 22:23:44 -04:00
GLaDOS
632500b734
Merge pull request #7555 from BlueWallet/hofffix
FIX: Handoff type wasnt being passed
2025-02-16 10:54:02 +00:00
Marcos Rodriguez Vélez
a4a513f703
Merge branch 'master' into cleanup 2025-02-15 16:13:51 -04:00
GLaDOS
39b141507c
Merge pull request #7593 from BlueWallet/tor
FIX: Allow onion preferred as user could be on Orbot
2025-02-15 18:40:43 +00:00
Marcos Rodriguez Velez
dde4520094 FIX: Slight cleanup 2025-02-15 14:07:37 -04:00
GLaDOS
a7843e127f
Merge pull request #7595 from BlueWallet/lighningse
REF: Reuse AddressInput component
2025-02-15 16:57:49 +00:00
Marcos Rodriguez Velez
f4125cb1e9 Merge branch 'master' into lighningse 2025-02-14 21:18:16 -04:00
GLaDOS
115b0a2a4f
Merge pull request #7594 from BlueWallet/wrap
REF: Wrap BlueElecrum promises in try catch
2025-02-14 20:31:10 +00:00
GLaDOS
7035bec229
Merge pull request #7590 from BlueWallet/translations_loc-en-json--master_pl
Updates for file loc/en.json in pl
2025-02-14 19:32:39 +00:00
Marcos Rodriguez Velez
80ef3252a1 REF: Wrap BlueElecrum promises in try catch 2025-02-14 15:24:20 -04:00
Marcos Rodriguez Velez
6cd6079493 FIX: Allow onion preferred as user could be on Orbot 2025-02-14 14:58:58 -04:00
GLaDOS
9a8158a384
Merge pull request #7529 from BlueWallet/psb
ADD: Test electrum connection prior to saving
2025-02-14 18:54:21 +00:00
GLaDOS
9976734665
Merge pull request #7591 from BlueWallet/allert
FIX: Confirm wallet delete alert would poptotop regardless of confirm…
2025-02-14 10:12:46 +00:00
Marcos Rodriguez Velez
1c15ae0a0c REF: Reuse AddressInput component 2025-02-13 18:38:37 -04:00
Marcos Rodriguez Velez
64d8238872 FIX: Confirm wallet delete alert would poptotop regardless of confirmation 2025-02-13 18:23:47 -04:00
transifex-integration[bot]
336785e1a0
Translate loc/en.json in pl
100% reviewed source file: 'loc/en.json'
on 'pl'.
2025-02-13 15:07:46 +00:00
GLaDOS
d68b806b60
Merge pull request #7586 from BlueWallet/tipbox
REF: TIpBox
2025-02-12 23:07:43 +00:00
GLaDOS
1acf4c9af2
Merge pull request #7587 from BlueWallet/rn
OPS: RN version bump
2025-02-12 23:07:40 +00:00
Marcos Rodriguez Velez
2d51238d6f OPS: RN version bump 2025-02-12 14:03:15 -04:00
Marcos Rodriguez Velez
3b90c49d79 REF: TIpBox 2025-02-12 13:56:12 -04:00
GLaDOS
226d499603
Merge pull request #7583 from BlueWallet/log
FIX: Remove annoying warning
2025-02-12 14:36:47 +00:00
GLaDOS
f155b6b577
Merge pull request #7585 from BlueWallet/locsync19
fix: sync language files
2025-02-12 13:11:26 +00:00
Ivan Vershigora
4be7f78be8
fix: sync language files 2025-02-12 11:21:11 +00:00
GLaDOS
5d81a4cf57
Merge pull request #7580 from BlueWallet/ale
FIX: Alert failed to show in some scenarios
2025-02-12 09:07:26 +00:00
GLaDOS
1b11200a0a
Merge pull request #7582 from BlueWallet/SIGN
FIX: handlePsbtSign();
2025-02-12 09:07:22 +00:00
Marcos Rodriguez Velez
f229beb5e0 FIX: Better erro handling for refreshAllWalletTransactions 2025-02-12 00:42:38 -04:00
Marcos Rodriguez Velez
e176783a3f FIX: Remove annoying warning 2025-02-12 00:23:11 -04:00
Marcos Rodriguez Velez
5c7460d6b1 Update SendDetails.tsx 2025-02-12 00:09:08 -04:00
Marcos Rodriguez Velez
571b056854 FIX: handlePsbtSign(); 2025-02-12 00:08:05 -04:00
Marcos Rodriguez Vélez
54db4b366e
FIX:Indicator color (#7581) 2025-02-11 20:39:48 -04:00
Marcos Rodriguez Velez
02dd22b8d4 Update StorageProvider.tsx 2025-02-11 17:55:46 -04:00
Marcos Rodriguez Velez
8b94a9db10 FIX: Alert failed to show in some scenarios 2025-02-11 17:55:06 -04:00
GLaDOS
175a5f27aa
Merge pull request #7579 from BlueWallet/translations_loc-en-json--master_es_419
Updates for file loc/en.json in es_419
2025-02-11 20:07:40 +00:00
GLaDOS
907e54938c
Merge pull request #7578 from BlueWallet/scanningspeed
FIX: Scanning speed was limited by unused prop
2025-02-11 19:56:20 +00:00
transifex-integration[bot]
c43d36d84d
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2025-02-11 19:09:44 +00:00
Marcos Rodriguez Velez
f900f46deb FIX: Scanning speed was limited by unused prop 2025-02-11 15:01:30 -04:00
GLaDOS
472b6c97ff
Merge pull request #7542 from BlueWallet/race
fix: race condition error in HD wallet fetchTransactions
2025-02-10 19:43:25 +00:00
GLaDOS
ae80cb9118
Merge pull request #7577 from BlueWallet/notifre
FIX: Alert the user if notifications deregister failed
2025-02-10 19:06:25 +00:00
GLaDOS
f389af09ba
Merge pull request #7536 from BlueWallet/vvi
DEL: File not used
2025-02-10 18:56:25 +00:00
Ivan Vershigora
fc5eccfcd4 fix: race condition error in HD wallet fetchTransactions 2025-02-10 18:51:18 +00:00
Marcos Rodriguez Velez
307306f5ec Merge branch 'master' into notifre 2025-02-10 14:24:27 -04:00
GLaDOS
73081033ed
Merge pull request #7576 from BlueWallet/fco
ADD: Single button container should be round as per design
2025-02-10 15:49:13 +00:00
Marcos Rodriguez Vélez
8b531350af
Update WalletDetails.tsx 2025-02-10 10:50:07 -04:00
Marcos Rodriguez Velez
4c09a52e02 wip 2025-02-10 10:07:28 -04:00
Marcos Rodriguez Velez
879f4f4081 FIX: Alert the user if notifications deregister failed 2025-02-10 09:49:01 -04:00
Marcos Rodriguez Velez
ea870729d6 ADD: Single button container should be round as per design 2025-02-09 23:52:37 -04:00
Marcos Rodriguez Velez
b8a8986a8e OPS:Version bump 2025-02-09 14:27:27 -04:00
Marcos Rodriguez Velez
caf41400a0 Update WalletTransactions.tsx 2025-02-09 12:03:39 -04:00
GLaDOS
30ed2a3d96
Merge pull request #7574 from BlueWallet/grad
FIX: Lower the height of the background behind refresh controller
2025-02-09 15:27:26 +00:00
Marcos Rodriguez Velez
a8374ab25b Update WalletTransactions.tsx 2025-02-09 10:35:47 -04:00
Marcos Rodriguez Velez
d3fd15dcf8 Update WalletTransactions.tsx 2025-02-09 09:46:44 -04:00
Marcos Rodriguez Velez
e8c181359d FIX: Lower the height of the background behind refresh controller 2025-02-09 09:16:09 -04:00
GLaDOS
307f6881c9
Merge pull request #7573 from BlueWallet/ELE
FIX: PReferred server would not save if it shared host name with existing one
2025-02-09 12:34:35 +00:00
Marcos Rodriguez Velez
e38e70bb0b FIX: PReferred server would not save if it shared host name 2025-02-08 22:23:03 -04:00
GLaDOS
92fda5d969
Merge pull request #7571 from BlueWallet/psbt
FIX: Can not sign exported psbt in ColdCard  #7490
2025-02-09 02:12:06 +00:00
Marcos Rodriguez Velez
e69c105ccf Update psbtMultisig.js 2025-02-08 20:59:51 -04:00
Marcos Rodriguez Velez
002efdc4e7 wip 2025-02-08 19:30:33 -04:00
Marcos Rodriguez Velez
63ab4da34b Update psbtMultisigQRCode.js 2025-02-08 18:46:08 -04:00
Marcos Rodriguez Velez
c2ff24591e Update psbtMultisig.js 2025-02-08 18:43:23 -04:00
Marcos Rodriguez Velez
19dd1047d1 Update psbtMultisig.js 2025-02-08 18:40:19 -04:00
Marcos Rodriguez Velez
38d92a7a5c Update psbtMultisigQRCode.js 2025-02-08 18:38:52 -04:00
Marcos Rodriguez Velez
59f2835cb8 Merge branch 'master' into psbt 2025-02-08 18:27:16 -04:00
GLaDOS
9863dfd47b
Merge pull request #7572 from BlueWallet/foc
FIX: Crash when pressing MAX on send details
2025-02-08 22:27:00 +00:00
Marcos Rodriguez Velez
70e32c9d69 Update psbtMultisigQRCode.js 2025-02-08 18:25:16 -04:00
Marcos Rodriguez Velez
2b393ba997 numbers 2025-02-08 18:15:17 -04:00
Marcos Rodriguez Velez
88a1ec4260 wip 2025-02-08 18:02:26 -04:00
Marcos Rodriguez Velez
5eabded72b Update SendDetails.tsx 2025-02-08 03:00:10 -04:00
Marcos Rodriguez Vélez
d957ee7197
Update SendDetails.tsx 2025-02-08 01:12:24 -04:00
Marcos Rodriguez Velez
a4df48a0c5 wip 2025-02-07 20:14:02 -04:00
Marcos Rodriguez Velez
6d7e48eb1a wip 2025-02-07 20:11:40 -04:00
Marcos Rodriguez Velez
0481c8d6a9 Update SendDetails.tsx 2025-02-07 20:07:08 -04:00
Marcos Rodriguez Velez
85e47ac83d Update psbtMultisigQRCode.js 2025-02-07 20:03:19 -04:00
Marcos Rodriguez Vélez
0d64347813
Update en.json 2025-02-07 18:01:22 -04:00
Marcos Rodriguez Velez
4849042dc6 wip 2025-02-07 13:16:41 -04:00
GLaDOS
d2e186bbf7
Merge pull request #7568 from BlueWallet/Nav
FIX: Dont run checks in ScanQrcode
2025-02-07 11:37:29 +00:00
Marcos Rodriguez Velez
588da24f0b FIX: Crash when pressing MAX on send details 2025-02-06 23:45:43 -04:00
Marcos Rodriguez Velez
a8858833ef FIX: Can not sign exported psbt in ColdCard #7490 2025-02-06 23:43:48 -04:00
GLaDOS
06ec5feb4b
Merge pull request #7570 from BlueWallet/translations_loc-en-json--master_fr_FR
Updates for file loc/en.json in fr_FR
2025-02-06 01:36:42 +00:00
transifex-integration[bot]
e9c3e3143f
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-02-05 16:54:13 +00:00
GLaDOS
83f545ed4b
Merge pull request #7565 from BlueWallet/qrui
FIX: ScanQR UI fixes
2025-02-05 15:39:33 +00:00
GLaDOS
7b8b3a0be2
Merge pull request #7567 from BlueWallet/swipe
FIX: Swiping is now easier
2025-02-05 15:39:29 +00:00
junderw
6dde0c4b4e Add support for importing minikeys (Casascius Coin, Satori Coin etc.) 2025-02-05 08:41:08 +00:00
Marcos Rodriguez Vélez
e810baf9c8
FIX: Dont run checks in ScanQrcode 2025-02-05 01:04:30 -04:00
Marcos Rodriguez Vélez
facd7b7783
Update ScanQRCode.js 2025-02-05 00:57:02 -04:00
Marcos Rodriguez Velez
89e7b23c05 Update PromptPasswordConfirmationModal.tsx 2025-02-05 00:35:52 -04:00
Marcos Rodriguez Velez
56c983e1dc Update ManageWalletsListItem.tsx 2025-02-05 00:27:16 -04:00
Marcos Rodriguez Velez
016e9f4214 Update ManageWalletsListItem.tsx 2025-02-04 23:33:51 -04:00
Marcos Rodriguez Velez
c932d0da5a FIX: Swiping is now easier 2025-02-04 23:14:00 -04:00
Marcos Rodriguez Velez
cdd923db7c ADD: TX Details action action 2025-02-04 22:32:04 -04:00
Marcos Rodriguez Velez
4611c46d1e FIX: ScanQR UI fixes 2025-02-04 21:54:24 -04:00
GLaDOS
59c9edeebd
Merge pull request #7561 from BlueWallet/txheader
REF: Wallet info header in Transactions
2025-02-04 17:33:37 +00:00
GLaDOS
05eff5f2b4
Merge pull request #7554 from BlueWallet/import
FIX: Import flow was holding on to previous reference
2025-02-04 17:22:30 +00:00
GLaDOS
8894bcf965
Merge pull request #7556 from BlueWallet/Issue---Function-to-broadcast-transaction-from-.final-PSBT-not-available-#7551
FIX: Issue - Function to broadcast transaction from *.final PSBT not …
2025-02-04 17:22:25 +00:00
Marcos Rodriguez Velez
a6306c53d8 FIX: Lndhub lacked timeouts 2025-02-03 01:29:48 -04:00
Marcos Rodriguez Velez
d05d51237a Update WalletTransactions.tsx 2025-02-02 20:49:03 -04:00
Marcos Rodriguez Vélez
5c70faf17d
Update WalletTransactions.tsx 2025-02-02 18:20:57 -04:00
Marcos Rodriguez Velez
18c5e38d6c wip 2025-02-02 16:51:32 -04:00
Marcos Rodriguez Velez
ca1be7d443 REF: Wallet info header in Transactions 2025-02-02 15:21:12 -04:00
GLaDOS
ec027a12df
Merge pull request #7560 from BlueWallet/translations_loc-en-json--master_de_DE
Updates for file loc/en.json in de_DE
2025-02-02 17:07:56 +00:00
transifex-integration[bot]
6b013e5bb7
Translate loc/en.json in de_DE
100% reviewed source file: 'loc/en.json'
on 'de_DE'.
2025-02-02 09:52:41 +00:00
Marcos Rodriguez Velez
950848181e Update SendDetails.tsx 2025-02-01 21:22:24 -04:00
Marcos Rodriguez Velez
559468b221 FIX: Issue - Function to broadcast transaction from *.final PSBT not available #7551 2025-02-01 21:08:35 -04:00
Marcos Rodriguez Velez
3adb90abff Update create.js 2025-02-01 10:51:37 -04:00
Marcos Rodriguez Vélez
6765dd7246
Update useHandoffListener.ts 2025-02-01 10:29:05 -04:00
Marcos Rodriguez Velez
6698645f48 Update SendDetails.tsx 2025-01-31 19:28:13 -04:00
Marcos Rodriguez Vélez
cdd76db18f
Merge branch 'master' into import 2025-01-31 19:19:15 -04:00
Marcos Rodriguez Velez
715991b106 FIX: Handoff type wasnt being passed 2025-01-31 18:49:22 -04:00
Marcos Rodriguez Velez
7882716c73 Update package-lock.json 2025-01-31 18:21:06 -04:00
Marcos Rodriguez Velez
9f912c51ed Merge branch 'master' into import 2025-01-31 18:14:35 -04:00
Marcos Rodriguez Velez
8ae9ac6155 OPS: Version bump 2025-01-31 18:13:54 -04:00
Marcos Rodriguez Velez
ea4acc2556 FIX: Import flow was holding on to previous reference 2025-01-31 18:13:06 -04:00
Marcos Rodriguez Velez
26c9449f2d Update project.pbxproj 2025-01-30 16:41:31 -04:00
Marcos Rodriguez Vélez
b21cf6e0ec
FIX: Multisig getID and navigation (#7549) 2025-01-30 15:37:01 -04:00
GLaDOS
ce18286d45
Merge pull request #7550 from BlueWallet/translations_loc-en-json--master_es_419
Updates for file loc/en.json in es_419
2025-01-30 19:16:35 +00:00
transifex-integration[bot]
d46d16140a
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2025-01-30 10:27:38 +00:00
GLaDOS
5aef6382b9
Merge pull request #7537 from BlueWallet/toolti
FIX: Entropy menu selectio
2025-01-30 09:57:52 +00:00
GLaDOS
16d418a6c1
Merge pull request #7548 from BlueWallet/translations_loc-en-json--master_es_419
Updates for file loc/en.json in es_419
2025-01-30 03:25:43 +00:00
Marcos Rodriguez Velez
4fc57adaac Update CommonToolTipActions.ts 2025-01-29 21:31:46 -04:00
Marcos Rodriguez Velez
8c504a1bd1 Merge branch 'master' into toolti 2025-01-29 21:13:51 -04:00
transifex-integration[bot]
e197fd70d4
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2025-01-29 20:35:53 +00:00
GLaDOS
342a127f99
Merge pull request #7545 from BlueWallet/renovate/react-native-community-cli-platform-ios-15.x
Update dependency @react-native-community/cli-platform-ios to v15.1.3
2025-01-29 02:51:14 +00:00
GLaDOS
a41032cfda
Merge pull request #7546 from BlueWallet/Bug---Operational-Security-Concern.-Electrum-Preferred-Server-Not-Persisting-/-Resetting-to-Suggested-Servers-#7532
FIX: Bug - Operational Security Concern. Electrum Preferred Server No…
2025-01-28 21:21:23 +00:00
Marcos Rodriguez Velez
0675e6ea62 wip 2025-01-28 16:23:15 -04:00
Marcos Rodriguez Velez
19ba071af3 Merge branch 'master' into Bug---Operational-Security-Concern.-Electrum-Preferred-Server-Not-Persisting-/-Resetting-to-Suggested-Servers-#7532 2025-01-28 16:23:08 -04:00
Marcos Rodriguez Velez
34b7525cba Revert "Update bluewallet.spec.js"
This reverts commit d8d97b2b39.
2025-01-28 10:51:26 -04:00
Marcos Rodriguez Velez
86c0d9d53d Merge branch 'master' into Bug---Operational-Security-Concern.-Electrum-Preferred-Server-Not-Persisting-/-Resetting-to-Suggested-Servers-#7532 2025-01-28 10:51:10 -04:00
Marcos Rodriguez Vélez
d8d97b2b39
Update bluewallet.spec.js 2025-01-28 03:40:28 -04:00
Marcos Rodriguez Velez
2503cb7882 FIX: Bug - Operational Security Concern. Electrum Preferred Server Not Persisting / Resetting to Suggested Servers #7532 2025-01-27 23:32:22 -04:00
renovate[bot]
ca912377bc
Update dependency @react-native-community/cli-platform-ios to v15.1.3 2025-01-28 02:13:43 +00:00
GLaDOS
cea31518dc
Merge pull request #7543 from BlueWallet/translations_loc-en-json--master_es_419
Updates for file loc/en.json in es_419
2025-01-28 02:05:49 +00:00
GLaDOS
d09e8ff68c
Merge pull request #7544 from BlueWallet/renovate/bugsnag-js-monorepo
Update dependency @bugsnag/react-native to v8.2.0
2025-01-28 02:05:46 +00:00
renovate[bot]
346581b3e2
Update dependency @bugsnag/react-native to v8.2.0 2025-01-27 13:57:15 +00:00
GLaDOS
10f7e44232
Merge pull request #7541 from BlueWallet/renovate/react-native-community-cli-platform-android-15.x
Update dependency @react-native-community/cli-platform-android to v15.1.3
2025-01-27 13:48:52 +00:00
transifex-integration[bot]
faf86028ab
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2025-01-27 13:29:09 +00:00
renovate[bot]
76c4023592
Update dependency @react-native-community/cli-platform-android to v15.1.3 2025-01-27 05:11:13 +00:00
GLaDOS
bd42acb7c7
Merge pull request #7540 from BlueWallet/renovate/react-native-community-cli-15.x
Update dependency @react-native-community/cli to v15.1.3
2025-01-27 05:02:28 +00:00
renovate[bot]
3d9e9ddf88
Update dependency @react-native-community/cli to v15.1.3 2025-01-26 22:48:27 +00:00
GLaDOS
0c88e0e9db
Merge pull request #7539 from BlueWallet/renovate/react-native-gesture-handler-2.x
Update dependency react-native-gesture-handler to v2.22.1
2025-01-26 22:39:57 +00:00
GLaDOS
0371e3352f
Merge pull request #7514 from BlueWallet/multi
REF: MultipleStepsListItem to TSX
2025-01-26 22:31:52 +00:00
Marcos Rodriguez Velez
4607d4a796 Merge branch 'master' into multi 2025-01-26 17:35:51 -04:00
renovate[bot]
4a44989a8f
Update dependency react-native-gesture-handler to v2.22.1 2025-01-26 20:03:40 +00:00
GLaDOS
0f6582e050
Merge pull request #7538 from BlueWallet/renovate/react-native-device-info-14.x
Update dependency react-native-device-info to v14.0.2
2025-01-26 19:59:36 +00:00
renovate[bot]
279afa517f
Update dependency react-native-device-info to v14.0.2 2025-01-26 16:38:27 +00:00
Marcos Rodriguez Velez
e1202c6854 Update ElectrumSettings.tsx 2025-01-26 12:29:52 -04:00
Marcos Rodriguez Velez
021a1fd352 Merge branch 'master' into psb 2025-01-26 12:28:13 -04:00
Marcos Rodriguez Velez
d51010dd85 FIX: Entropy menu selectio 2025-01-26 11:33:03 -04:00
Marcos Rodriguez Velez
8f26859f76 DEL: File not used 2025-01-26 10:48:10 -04:00
GLaDOS
49c5e67f45
Merge pull request #7534 from BlueWallet/renovate/rn-qr-generator-digest
Update rn-qr-generator digest to 731ed8e
2025-01-26 12:07:50 +00:00
renovate[bot]
a4a6fa5ef4
Update rn-qr-generator digest to 731ed8e 2025-01-26 08:53:09 +00:00
GLaDOS
96b1331a60
Merge pull request #7199 from BlueWallet/rn76
OPS; Upgrade to RN 76
2025-01-26 08:32:22 +00:00
GLaDOS
791cbd5f94
Merge pull request #7516 from BlueWallet/sele
FIX: Allow text to be selectable
2025-01-26 08:32:01 +00:00
GLaDOS
073280225d
Merge pull request #7533 from BlueWallet/bs
FIX: BS spam detection complaint
2025-01-26 00:19:56 +00:00
Marcos Rodriguez Velez
077f3a3a04 Update build-release-apk.yml 2025-01-25 19:24:35 -04:00
Marcos Rodriguez Velez
1b73ab9b06 FIX: BS spam detection complaint 2025-01-25 19:08:58 -04:00
GLaDOS
9acaac9646
Merge pull request #7531 from BlueWallet/entropyfix
FIX: Entropy menu actions construction
2025-01-25 22:41:46 +00:00
Marcos Rodriguez Velez
19bddcb152 Update Add.tsx 2025-01-25 17:44:25 -04:00
Marcos Rodriguez Velez
ddf00d5d44 FIX: Entropy menu actions construction 2025-01-25 17:30:49 -04:00
Marcos Rodriguez Vélez
721f0d3ecb
Update package.json 2025-01-25 17:10:34 -04:00
Marcos Rodriguez Vélez
6561bb0524
Update package.json 2025-01-24 21:31:18 -04:00
thisames
8b32825e73
FIX: handle platform-specific icon compatibility for delete button (#7530) 2025-01-24 21:28:45 -04:00
Marcos Rodriguez Velez
2cdd01f2c2 wip 2025-01-24 21:03:03 -04:00
Marcos Rodriguez Velez
fc7eb4ece2 wip 2025-01-24 20:55:26 -04:00
Marcos Rodriguez Velez
486bc43202 Merge branch 'master' into rn76 2025-01-24 20:34:05 -04:00
Marcos Rodriguez Velez
fe37bcb9fd Update project.pbxproj 2025-01-24 20:32:51 -04:00
Marcos Rodriguez Velez
75a26d155c wip 2025-01-24 20:19:37 -04:00
Marcos Rodriguez Velez
38ab7665bc Merge branch 'master' into rn76 2025-01-24 20:19:30 -04:00
Marcos Rodriguez Velez
c3ae3c8104 OPS: Version bump 2025-01-24 20:09:32 -04:00
Marcos Rodriguez Velez
f6a6d7c41e ADD: Test electrum connection prior to saving 2025-01-24 00:22:20 -04:00
Marcos Rodriguez Velez
cf16417e00 FIX: Allow text to be selectable 2025-01-19 23:42:15 -04:00
Marcos Rodriguez Velez
40c5cc7295 REF: MultipleStepsListItem to TSX 2025-01-19 15:12:35 -04:00
Marcos Rodriguez Velez
f1a7fc8c40 wip 2024-10-26 00:34:29 -04:00
Marcos Rodriguez Velez
7a218a4fa6 Merge branch 'master' into rn76 2024-10-26 00:31:42 -04:00
Marcos Rodriguez Velez
5f2378a9de Merge branch 'realm' into rn76 2024-10-25 18:12:08 -04:00
Marcos Rodriguez Velez
9583fac4c6 Update package-lock.json 2024-10-24 01:00:57 -04:00
Marcos Rodriguez Vélez
fac654c263
Merge branch 'master' into rn76 2024-10-24 00:17:04 -04:00
Marcos Rodriguez Vélez
17eba2d925
Update package.json 2024-10-23 19:22:49 -04:00
Marcos Rodriguez Vélez
2f4b688bfd
Update build.gradle 2024-10-23 19:21:35 -04:00
Marcos Rodriguez Velez
0c43cc7b24 OPS; Upgrade to RN 76 2024-10-23 18:58:52 -04:00
212 changed files with 9485 additions and 6405 deletions

View file

@ -47,6 +47,24 @@
"device": "emulator",
"app": "android.debug"
},
"android.debug.device": {
"device": {
"device": {
"adbName": ".*"
},
"type": "android.attached"
},
"app": "android.debug"
},
"android.release.device": {
"device": {
"device": {
"adbName": ".*"
},
"type": "android.attached"
},
"app": "android.release"
},
"android.release": {
"device": "emulator",
"app": "android.release"

View file

@ -22,12 +22,40 @@ jobs:
branch_name: ${{ steps.get_latest_commit_details.outputs.branch_name }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
MATCH_READONLY: "true"
steps:
- name: Checkout Project
uses: actions/checkout@v4
with:
fetch-depth: 0 # Ensures the full Git history is available
- name: Setup Caching
uses: actions/cache@v3
with:
path: |
~/Library/Caches/CocoaPods
ios/Pods
~/.npm
node_modules
vendor/bundle
key: ${{ runner.os }}-ios-${{ hashFiles('**/package-lock.json', '**/Podfile.lock', '**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-ios-
- name: Clear All Caches
if: github.ref == 'refs/heads/master'
run: |
echo "Clearing Xcode DerivedData..."
rm -rf ~/Library/Developer/Xcode/DerivedData
echo "Clearing CocoaPods Cache..."
rm -rf ~/Library/Caches/CocoaPods
echo "Clearing npm Cache..."
npm cache clean --force
echo "Clearing Ruby Gems Cache..."
rm -rf ~/.gem
echo "Clearing Bundler Cache..."
rm -rf ~/.bundle/cache
- name: Ensure Correct Branch
if: github.ref != 'refs/heads/master'
@ -67,15 +95,32 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 16.0
xcode-version: latest
- name: Install iOS Simulator Runtime
run: |
echo "Available iOS simulator runtimes:"
xcrun simctl list runtimes
# Try to download the latest iOS 16.x simulator if not present
if (! xcrun simctl list runtimes | grep -q "iOS 16"); then
echo "Installing iOS 16.4 simulator..."
sudo xcode-select -s /Applications/Xcode.app
xcodebuild -downloadPlatform iOS
fi
echo "Available iOS simulator runtimes after install:"
xcrun simctl list runtimes
- name: Set Up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.6
bundler-cache: true
- name: Install Dependencies with Bundler
run: |
@ -88,6 +133,7 @@ jobs:
- name: Install CocoaPods Dependencies
run: |
bundle exec fastlane ios install_pods
echo "CocoaPods dependencies installed successfully"
- name: Generate Build Number Based on Timestamp
id: generate_build_number
@ -133,8 +179,26 @@ jobs:
- name: Build App
id: build_app
run: |
bundle exec fastlane ios build_app_lane --verbose
echo "ipa_output_path=$IPA_OUTPUT_PATH" >> $GITHUB_OUTPUT # Set the IPA output path for future jobs
bundle exec fastlane ios build_app_lane
# Ensure IPA path is set for subsequent steps
if [ -f "./ios/build/ipa_path.txt" ]; then
IPA_PATH=$(cat ./ios/build/ipa_path.txt)
echo "IPA_OUTPUT_PATH=$IPA_PATH" >> $GITHUB_ENV
echo "ipa_output_path=$IPA_PATH" >> $GITHUB_OUTPUT
echo "Found IPA at: $IPA_PATH"
else
echo "Warning: ipa_path.txt not found, trying to locate IPA file manually..."
IPA_PATH=$(find ./ios -name "*.ipa" | head -n 1)
if [ -n "$IPA_PATH" ]; then
echo "IPA_OUTPUT_PATH=$IPA_PATH" >> $GITHUB_ENV
echo "ipa_output_path=$IPA_PATH" >> $GITHUB_OUTPUT
echo "Found IPA at: $IPA_PATH"
else
echo "Error: No IPA file found"
exit 1
fi
fi
- name: Upload Bugsnag Sourcemaps
if: success()
@ -142,8 +206,8 @@ jobs:
env:
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
BUGSNAG_RELEASE_STAGE: production
PROJECT_VERSION: ${{ needs.build.outputs.project_version }}
NEW_BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }}
PROJECT_VERSION: ${{ env.PROJECT_VERSION }}
NEW_BUILD_NUMBER: ${{ env.NEW_BUILD_NUMBER }}
- name: Upload Build Logs
if: always()
@ -151,13 +215,32 @@ jobs:
with:
name: build_logs
path: ./ios/build_logs/
retention-days: 7
- name: Verify IPA File Before Upload
run: |
echo "Checking IPA file at: $IPA_OUTPUT_PATH"
if [ -f "$IPA_OUTPUT_PATH" ]; then
echo "✅ IPA file exists"
ls -la "$IPA_OUTPUT_PATH"
else
echo "❌ IPA file not found at: $IPA_OUTPUT_PATH"
echo "Current directory contents:"
find ./ios -name "*.ipa"
exit 1
fi
- name: Upload IPA as Artifact
if: success()
uses: actions/upload-artifact@v4
with:
name: BlueWallet_${{env.PROJECT_VERSION}}_${{env.NEW_BUILD_NUMBER}}.ipa
path: ${{ env.IPA_OUTPUT_PATH }} # Directly from Fastfile `IPA_OUTPUT_PATH`
name: BlueWallet_IPA
path: ${{ env.IPA_OUTPUT_PATH }}
retention-days: 7
- name: Delete Temporary Keychain
if: always()
run: bundle exec fastlane ios delete_temp_keychain
testflight-upload:
needs: build
@ -177,6 +260,7 @@ jobs:
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.6
bundler-cache: true
- name: Install Dependencies with Bundler
run: |
@ -186,18 +270,11 @@ jobs:
- name: Download IPA from Artifact
uses: actions/download-artifact@v4
with:
name: BlueWallet_${{ needs.build.outputs.project_version }}_${{ needs.build.outputs.new_build_number }}.ipa
name: BlueWallet_IPA
path: ./
- name: Create App Store Connect API Key JSON
run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' > ./appstore_api_key.json
- name: Verify IPA File Download
run: |
echo "Current directory:"
pwd
echo "Files in current directory:"
ls -la ./
- name: Set IPA Path Environment Variable
run: echo "IPA_OUTPUT_PATH=$(pwd)/BlueWallet_${{ needs.build.outputs.project_version }}_${{ needs.build.outputs.new_build_number }}.ipa" >> $GITHUB_ENV
@ -205,19 +282,23 @@ jobs:
- name: Verify IPA Path Before Upload
run: |
if [ ! -f "$IPA_OUTPUT_PATH" ]; then
echo "IPA file not found at path: $IPA_OUTPUT_PATH"
echo "❌ IPA file not found at path: $IPA_OUTPUT_PATH"
ls -la $(pwd)
exit 1
else
echo "✅ Found IPA at: $IPA_OUTPUT_PATH"
fi
- name: Print Environment Variables for Debugging
run: |
echo "LATEST_COMMIT_MESSAGE: $LATEST_COMMIT_MESSAGE"
echo "BRANCH_NAME: $BRANCH_NAME"
echo "PROJECT_VERSION: $PROJECT_VERSION"
echo "NEW_BUILD_NUMBER: $NEW_BUILD_NUMBER"
echo "IPA_OUTPUT_PATH: $IPA_OUTPUT_PATH"
- name: Upload to TestFlight
run: |
ls -la $IPA_OUTPUT_PATH
bundle exec fastlane ios upload_to_testflight_lane
run: bundle exec fastlane ios upload_to_testflight_lane
env:
APP_STORE_CONNECT_API_KEY_PATH: $(pwd)/appstore_api_key.p8
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
@ -228,18 +309,19 @@ jobs:
APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }}
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
IPA_OUTPUT_PATH: ${{ env.IPA_OUTPUT_PATH }}
- name: Post PR Comment
if: success() && github.event_name == 'pull_request'
uses: actions/github-script@v6
env:
BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }}
PROJECT_VERSION: ${{ needs.build.outputs.project_version }}
LATEST_COMMIT_MESSAGE: ${{ needs.build.outputs.latest_commit_message }}
with:
script: |
const buildNumber = process.env.BUILD_NUMBER;
const message = `The build ${buildNumber} has been uploaded to TestFlight.`;
const version = process.env.PROJECT_VERSION;
const message = `✅ Build ${version} (${buildNumber}) has been uploaded to TestFlight and will be available for testing soon.`;
const prNumber = context.payload.pull_request.number;
const repo = context.repo;
github.rest.issues.createComment({

View file

@ -4,7 +4,7 @@ on:
pull_request:
branches:
- master
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled, unlabeled]
push:
branches:
- master
@ -102,7 +102,7 @@ jobs:
browserstack:
runs-on: ubuntu-latest
needs: buildReleaseApk
if: ${{ github.event_name == 'pull_request' }}
if: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'browserstack') }}
steps:
- name: Checkout code

17
App.tsx
View file

@ -1,5 +1,3 @@
import 'react-native-gesture-handler'; // should be on top
import { NavigationContainer } from '@react-navigation/native';
import React from 'react';
import { useColorScheme } from 'react-native';
@ -9,23 +7,26 @@ import { SettingsProvider } from './components/Context/SettingsProvider';
import { BlueDarkTheme, BlueDefaultTheme } from './components/themes';
import MasterView from './navigation/MasterView';
import { navigationRef } from './NavigationService';
import { useLogger } from '@react-navigation/devtools';
import { StorageProvider } from './components/Context/StorageProvider';
const App = () => {
const colorScheme = useColorScheme();
useLogger(navigationRef);
return (
<LargeScreenProvider>
<NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}>
<SafeAreaProvider>
<NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}>
<SafeAreaProvider>
<LargeScreenProvider>
<StorageProvider>
<SettingsProvider>
<MasterView />
</SettingsProvider>
</StorageProvider>
</SafeAreaProvider>
</NavigationContainer>
</LargeScreenProvider>
</LargeScreenProvider>
</SafeAreaProvider>
</NavigationContainer>
);
};

View file

@ -40,9 +40,16 @@ export const BlueCard = props => {
return <View {...props} style={{ padding: 20 }} />;
};
export const BlueText = props => {
export const BlueText = ({ bold = false, ...props }) => {
const { colors } = useTheme();
const style = StyleSheet.compose({ color: colors.foregroundColor, writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr' }, props.style);
const style = StyleSheet.compose(
{
color: colors.foregroundColor,
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
fontWeight: bold ? 'bold' : 'normal',
},
props.style,
);
return <Text {...props} style={style} />;
};
@ -75,6 +82,7 @@ export const BlueFormMultiInput = props => {
multiline
underlineColorAndroid="transparent"
numberOfLines={4}
editable={!props.editable}
style={{
paddingHorizontal: 8,
paddingVertical: 16,

10
Gemfile
View file

@ -3,8 +3,14 @@ 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 'rubyzip', '2.4.1'
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'cocoapods', '~> 1.14.3'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem "fastlane", ">= 2.225.0"
gem "fastlane", "~> 2.226.0"
gem 'xcodeproj', '< 1.26.0'
gem 'concurrent-ruby', '< 1.3.4'
# Required for App Store Connect API
gem "jwt"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

View file

@ -5,7 +5,7 @@ GEM
base64
nkf
rexml
activesupport (7.2.2)
activesupport (7.2.2.1)
base64
benchmark (>= 0.3)
bigdecimal
@ -24,31 +24,32 @@ GEM
json (>= 1.5.1)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.1002.0)
aws-sdk-core (3.212.0)
aws-eventstream (1.3.1)
aws-partitions (1.1058.0)
aws-sdk-core (3.219.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.95.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (1.99.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.170.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-s3 (1.182.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.1)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
benchmark (0.4.0)
bigdecimal (3.1.8)
bigdecimal (3.1.9)
claide (1.1.0)
cocoapods (1.16.2)
cocoapods (1.14.3)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.16.2)
cocoapods-core (= 1.14.3)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 2.1, < 3.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
@ -62,8 +63,8 @@ GEM
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
xcodeproj (>= 1.27.0, < 2.0)
cocoapods-core (1.16.2)
xcodeproj (>= 1.23.0, < 2.0)
cocoapods-core (1.14.3)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
algoliasearch (~> 1.0)
@ -86,10 +87,10 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
concurrent-ruby (1.3.3)
connection_pool (2.5.0)
declarative (0.0.20)
digest-crc (0.6.5)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
@ -118,8 +119,8 @@ GEM
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-multipart (1.1.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
@ -127,8 +128,8 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.225.0)
fastimage (2.4.0)
fastlane (2.226.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@ -168,17 +169,25 @@ GEM
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty (~> 0.4.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-browserstack (0.3.3)
rest-client (~> 2.0, >= 2.0.2)
fastlane-plugin-bugsnag (2.3.1)
git
xml-simple
fastlane-plugin-bugsnag_sourcemaps_upload (0.2.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
ffi (1.17.0)
ffi (1.17.1)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
git (3.0.0)
activesupport (>= 5.0)
addressable (~> 2.8)
process_executer (~> 1.3)
rchardet (~> 1.9)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
@ -217,36 +226,40 @@ GEM
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-accept (1.7.0)
http-cookie (1.0.7)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (1.14.6)
httpclient (2.9.0)
mutex_m
i18n (1.14.7)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
json (2.8.1)
jwt (2.9.3)
json (2.10.1)
jwt (2.10.1)
base64
logger (1.6.1)
logger (1.6.6)
mime-types (3.6.0)
logger
mime-types-data (~> 3.2015)
mime-types-data (3.2024.1105)
mime-types-data (3.2025.0220)
mini_magick (4.13.2)
mini_mime (1.1.5)
minitest (5.25.1)
minitest (5.25.4)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.4.1)
nanaimo (0.4.0)
mutex_m (0.3.0)
nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.1)
netrc (0.11.0)
nkf (0.2.0)
optparse (0.5.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.1)
plist (3.7.2)
process_executer (1.3.0)
public_suffix (4.0.7)
rake (13.2.1)
rchardet (1.9.0)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
@ -257,12 +270,12 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (3.1.2)
rexml (3.3.9)
rouge (2.0.7)
rexml (3.4.1)
rouge (3.28.0)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
securerandom (0.3.1)
securerandom (0.4.1)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
@ -288,31 +301,37 @@ GEM
uber (0.1.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.27.0)
xcodeproj (1.25.1)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
nanaimo (~> 0.3.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty (0.4.0)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
xml-simple (1.1.9)
rexml
PLATFORMS
ruby
DEPENDENCIES
activesupport (>= 6.1.7.5, != 7.1.0)
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
fastlane (>= 2.225.0)
cocoapods (~> 1.14.3)
concurrent-ruby (< 1.3.4)
fastlane (~> 2.226.0)
fastlane-plugin-browserstack
fastlane-plugin-bugsnag
fastlane-plugin-bugsnag_sourcemaps_upload
jwt
rubyzip (= 2.4.1)
xcodeproj (< 1.26.0)
RUBY VERSION
ruby 3.1.6p260
BUNDLED WITH
2.5.18
2.3.27

View file

@ -1,23 +0,0 @@
import 'react-native-gesture-handler'; // should be on top
import React, { lazy, Suspense } from 'react';
import MainRoot from './navigation';
import { useStorage } from './hooks/context/useStorage';
const CompanionDelegates = lazy(() => import('./components/CompanionDelegates'));
const MasterView = () => {
const { walletsInitialized } = useStorage();
return (
<>
<MainRoot />
{walletsInitialized && (
<Suspense>
<CompanionDelegates />
</Suspense>
)}
</>
);
};
export default MasterView;

View file

@ -14,10 +14,6 @@ export function dispatch(action: NavigationAction) {
}
}
export function navigateToWalletsList() {
navigate('WalletsList');
}
export function reset() {
if (navigationRef.isReady()) {
navigationRef.current?.reset({

View file

@ -73,6 +73,10 @@ def enableProguardInReleaseBuilds = false
def jscFlavor = 'org.webkit:android-jsc-intl:+'
android {
androidResources {
noCompress += ["bundle"]
}
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdkVersion rootProject.ext.compileSdkVersion
@ -83,7 +87,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "7.0.8"
versionName "7.1.5"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
@ -133,7 +137,7 @@ dependencies {
androidTestImplementation('com.wix:detox:0.1.1')
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
}
apply plugin: 'com.google.gms.google-services' // Google Services plugin
apply plugin: "com.bugsnag.android.gradle"
apply plugin: "com.bugsnag.android.gradle"

View file

@ -16,13 +16,6 @@
<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.ACTION_OPEN_DOCUMENT" />
<uses-permission android:name="android.permission.ACTION_GET_CONTENT" />
<uses-permission android:name="android.permission.ACTION_CREATE_DOCUMENT" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.ACTION_SCREEN_OFF"/>
<uses-permission android:name="android.permission.ACTION_SCREEN_ON"/>
<application
android:name=".MainApplication"
@ -58,7 +51,15 @@
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
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

View file

@ -293,6 +293,13 @@
"symbol": "zł",
"country": "Poland (Polish Zloty)"
},
"PYG": {
"endPointKey": "PYG",
"locale": "es-PY",
"source": "CoinDesk",
"symbol": "₲",
"country": "Paraguay (Paraguayan Guarani)"
},
"QAR": {
"endPointKey": "QAR",
"locale": "ar-QA",

View file

@ -4,6 +4,7 @@ import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.work.WorkManager
class BitcoinPriceWidget : AppWidgetProvider() {
@ -11,7 +12,6 @@ class BitcoinPriceWidget : AppWidgetProvider() {
companion object {
private const val TAG = "BitcoinPriceWidget"
private const val SHARED_PREF_NAME = "group.io.bluewallet.bluewallet"
private const val WIDGET_COUNT_KEY = "widget_count"
}
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
@ -24,22 +24,12 @@ class BitcoinPriceWidget : AppWidgetProvider() {
override fun onEnabled(context: Context) {
super.onEnabled(context)
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 0)
if (widgetCount >= 1) {
Log.e(TAG, "Only one widget instance is allowed.")
return
}
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount + 1).apply()
Log.d(TAG, "onEnabled called")
WidgetUpdateWorker.scheduleWork(context)
}
override fun onDisabled(context: Context) {
super.onDisabled(context)
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 1)
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount - 1).apply()
Log.d(TAG, "onDisabled called")
clearCache(context)
WorkManager.getInstance(context).cancelUniqueWork(WidgetUpdateWorker.WORK_NAME)
@ -47,9 +37,6 @@ class BitcoinPriceWidget : AppWidgetProvider() {
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 1)
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount - appWidgetIds.size).apply()
Log.d(TAG, "onDeleted called for widgets: ${appWidgetIds.joinToString()}")
}

View file

@ -12,6 +12,7 @@ import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
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.react.modules.i18nmanager.I18nUtil
@ -50,8 +51,7 @@ class MainApplication : Application(), ReactApplication {
sharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
val sharedI18nUtilInstance = I18nUtil.getInstance()
sharedI18nUtilInstance.allowRTL(applicationContext, true)
SoLoader.init(this, /* native exopackage */ false)
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()

View file

@ -56,7 +56,8 @@ object MarketAPI {
"CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.lowercase()}"
"BNR" -> "https://www.bnr.ro/nbrfxrates.xml"
"Kraken" -> "https://api.kraken.com/0/public/Ticker?pair=XXBTZ${endPointKey.uppercase()}"
else -> "https://api.coindesk.com/v1/bpi/currentprice/$endPointKey.json"
"CoinDesk" -> "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=${endPointKey.uppercase()}"
else -> "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=${endPointKey.uppercase()}"
}
}
}
@ -73,6 +74,10 @@ object MarketAPI {
"coinpaprika" -> json.getJSONObject("quotes").getJSONObject("INR").getString("price")
"Coinbase" -> json.getJSONObject("data").getString("amount")
"Kraken" -> json.getJSONObject("result").getJSONObject("XXBTZ${endPointKey.uppercase()}").getJSONArray("c").getString(0)
"CoinDesk" -> {
val rate = json.optDouble(endPointKey.uppercase(), -1.0)
if (rate < 0) null else rate.toString()
}
else -> null
}
} catch (e: Exception) {

View file

@ -56,7 +56,7 @@
<TextView
android:id="@+id/price_value"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
@ -67,6 +67,7 @@
android:autoSizeTextType="uniform"
android:duplicateParentState="false"
android:editable="false"
android:gravity="end"
android:lines="1"
android:text="Loading..."
android:textSize="24sp"

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_layout"
android:minWidth="160dp"
android:minWidth="170dp"
android:minHeight="100dp"
android:updatePeriodMillis="0"
android:widgetCategory="home_screen"

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="downloads" path="Download/" />
</paths>

View file

@ -3,8 +3,8 @@
buildscript {
ext {
minSdkVersion = 24
buildToolsVersion = "34.0.0"
compileSdkVersion = 34
buildToolsVersion = "35.0.0"
compileSdkVersion = 35
targetSdkVersion = 34
googlePlayServicesVersion = "16.+"
googlePlayServicesIidVersion = "16.0.1"
@ -58,8 +58,8 @@ subprojects {
afterEvaluate {project ->
if (project.hasProperty("android")) {
android {
buildToolsVersion "34.0.0"
compileSdkVersion 34
buildToolsVersion "35.0.0"
compileSdkVersion 35
defaultConfig {
minSdkVersion 24
}

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-8.10.2-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

5
android/gradlew vendored
View file

@ -86,7 +86,8 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -248,4 +249,4 @@ eval "set -- $(
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
exec "$JAVACMD" "$@"

188
android/gradlew.bat vendored
View file

@ -1,94 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
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 %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
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 %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -10,6 +10,7 @@ import loc from '../loc';
import { GROUP_IO_BLUEWALLET } from './currency';
import { ElectrumServerItem } from '../screen/settings/ElectrumSettings';
import { triggerWarningHapticFeedback } from './hapticFeedback';
import { AlertButton } from 'react-native';
const ElectrumClient = require('electrum-client');
const net = require('net');
@ -137,31 +138,40 @@ async function _getRealm() {
}
export const getPreferredServer = async (): Promise<ElectrumServerItem | undefined> => {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
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('Getting preferred server:', { host, tcpPort, sslPort });
if (!host) {
console.warn('Preferred server host is undefined');
return;
if (!host) {
console.warn('Preferred server host is undefined');
return;
}
return {
host,
tcp: tcpPort ? Number(tcpPort) : undefined,
ssl: sslPort ? Number(sslPort) : undefined,
};
} catch (error) {
console.error('Error in getPreferredServer:', error);
return undefined;
}
return {
host,
tcp: tcpPort ? Number(tcpPort) : undefined,
ssl: sslPort ? Number(sslPort) : undefined,
};
};
export const removePreferredServer = async () => {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
console.log('Removing preferred server');
await DefaultPreference.clear(ELECTRUM_HOST);
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
console.log('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);
}
};
export async function isDisabled(): Promise<boolean> {
@ -203,26 +213,31 @@ function getNextPeer() {
}
async function getSavedPeer(): Promise<Peer | null> {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
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('Getting saved peer:', { host, tcpPort, sslPort });
if (!host) {
if (!host) {
return null;
}
if (sslPort) {
return { host, ssl: Number(sslPort) };
}
if (tcpPort) {
return { host, tcp: Number(tcpPort) };
}
return null;
} catch (error) {
console.error('Error in getSavedPeer:', error);
return null;
}
if (sslPort) {
return { host, ssl: Number(sslPort) };
}
if (tcpPort) {
return { host, tcp: Number(tcpPort) };
}
return null;
}
export async function connectMain(): Promise<void> {
@ -238,19 +253,6 @@ export async function connectMain(): Promise<void> {
console.log('Using peer:', JSON.stringify(usingPeer));
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
try {
if (usingPeer.host.endsWith('onion')) {
const randomPeer = getCurrentPeer();
await DefaultPreference.set(ELECTRUM_HOST, randomPeer.host);
await DefaultPreference.set(ELECTRUM_TCP_PORT, randomPeer.tcp ?? '');
await DefaultPreference.set(ELECTRUM_SSL_PORT, randomPeer.ssl ?? '');
}
} catch (e) {
// Must be running on Android
console.log(e);
}
try {
console.log('begin connection:', JSON.stringify(usingPeer));
mainClient = new ElectrumClient(net, tls, usingPeer.ssl || usingPeer.tcp, usingPeer.host, usingPeer.ssl ? 'tls' : 'tcp');
@ -261,7 +263,8 @@ export async function connectMain(): Promise<void> {
// 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?.close();
mainClient = undefined;
mainConnected = false;
// dropping `mainConnected` flag ensures there wont be reconnection race condition if several
// errors triggered
@ -309,12 +312,15 @@ export async function connectMain(): Promise<void> {
} catch (e) {
mainConnected = false;
console.log('bad connection:', JSON.stringify(usingPeer), e);
mainClient?.close();
mainClient = undefined;
}
if (!mainConnected) {
console.log('retry');
connectionAttempt = connectionAttempt + 1;
mainClient.close && mainClient.close();
mainClient?.close();
mainClient = undefined;
if (connectionAttempt >= 5) {
presentNetworkErrorAlert(usingPeer);
} else {
@ -326,33 +332,61 @@ export async function connectMain(): Promise<void> {
}
export async function presentResetToDefaultsAlert(): Promise<boolean> {
const hasPreferredServer = await getPreferredServer();
const serverHistoryStr = await DefaultPreference.get(ELECTRUM_SERVER_HISTORY);
const serverHistory = typeof serverHistoryStr === 'string' ? JSON.parse(serverHistoryStr) : [];
return new Promise(resolve => {
triggerWarningHapticFeedback();
const buttons: AlertButton[] = [];
if (hasPreferredServer?.host && (hasPreferredServer.tcp || hasPreferredServer.ssl)) {
buttons.push({
text: loc.settings.electrum_reset,
onPress: async () => {
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.clear(ELECTRUM_HOST);
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
} catch (e) {
console.log(e); // Must be running on Android
}
resolve(true);
},
style: 'default',
});
}
if (serverHistory.length > 0) {
buttons.push({
text: loc.settings.electrum_reset_to_default_and_clear_history,
onPress: async () => {
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.clear(ELECTRUM_SERVER_HISTORY);
await DefaultPreference.clear(ELECTRUM_HOST);
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
} catch (e) {
console.log(e); // Must be running on Android
}
resolve(true);
},
style: 'destructive',
});
}
buttons.push({
text: loc._.cancel,
onPress: () => resolve(false),
style: 'cancel',
});
presentAlert({
title: loc.settings.electrum_reset,
message: loc.settings.electrum_reset_to_default,
buttons: [
{
text: loc._.cancel,
style: 'cancel',
onPress: () => resolve(false),
},
{
text: loc._.ok,
style: 'destructive',
onPress: async () => {
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
await DefaultPreference.clear(ELECTRUM_HOST);
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
} catch (e) {
console.log(e); // Must be running on Android
}
resolve(true);
},
},
],
buttons,
options: { cancelable: true },
});
});
@ -378,7 +412,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
text: loc.wallets.list_tryagain,
onPress: () => {
connectionAttempt = 0;
mainClient.close() && mainClient.close();
mainClient?.close();
mainClient = undefined;
setTimeout(connectMain, 500);
},
style: 'default',
@ -389,7 +424,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
presentResetToDefaultsAlert().then(result => {
if (result) {
connectionAttempt = 0;
mainClient.close() && mainClient.close();
mainClient?.close();
mainClient = undefined;
setTimeout(connectMain, 500);
}
});
@ -400,7 +436,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
text: loc._.cancel,
onPress: () => {
connectionAttempt = 0;
mainClient.close() && mainClient.close();
mainClient?.close();
mainClient = undefined;
},
style: 'cancel',
},
@ -445,13 +482,18 @@ async function getRandomDynamicPeer(): Promise<Peer> {
}
export const getBalanceByAddress = async function (address: string): Promise<{ confirmed: number; unconfirmed: number }> {
if (!mainClient) throw new Error('Electrum client is not connected');
const script = bitcoin.address.toOutputScript(address);
const hash = bitcoin.crypto.sha256(script);
const reversedHash = Buffer.from(hash).reverse();
const balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
balance.addr = address;
return balance;
try {
if (!mainClient) throw new Error('Electrum client is not connected');
const script = bitcoin.address.toOutputScript(address);
const hash = bitcoin.crypto.sha256(script);
const reversedHash = Buffer.from(hash).reverse();
const balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
balance.addr = address;
return balance;
} catch (error) {
console.error('Error in getBalanceByAddress:', error);
throw error;
}
};
export const getConfig = async function () {
@ -929,25 +971,29 @@ export async function multiGetTransactionByTxid<T extends boolean>(
}
// saving cache:
realm.write(() => {
for (const txid of Object.keys(ret)) {
const tx = ret[txid];
// dont cache immature txs, but only for 'verbose', since its fully decoded tx jsons. non-verbose are just plain
// strings txhex
if (verbose && typeof tx !== 'string' && (!tx?.confirmations || tx.confirmations < 7)) {
continue;
}
try {
realm.write(() => {
for (const txid of Object.keys(ret)) {
const tx = ret[txid];
// dont cache immature txs, but only for 'verbose', since its fully decoded tx jsons. non-verbose are just plain
// strings txhex
if (verbose && typeof tx !== 'string' && (!tx?.confirmations || tx.confirmations < 7)) {
continue;
}
realm.create(
'Cache',
{
cache_key: txid + cacheKeySuffix,
cache_value: JSON.stringify(ret[txid]),
},
Realm.UpdateMode.Modified,
);
}
});
realm.create(
'Cache',
{
cache_key: txid + cacheKeySuffix,
cache_value: JSON.stringify(ret[txid]),
},
Realm.UpdateMode.Modified,
);
}
});
} catch (writeError) {
console.error('Failed to write transaction cache:', writeError);
}
return ret;
}

View file

@ -1,9 +1,8 @@
import { Alert, Linking, Platform } from 'react-native';
import { Platform } from 'react-native';
import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
import { launchImageLibrary, ImagePickerResponse } from 'react-native-image-picker';
import Share from 'react-native-share';
import { request, PERMISSIONS } from 'react-native-permissions';
import presentAlert from '../components/Alert';
import loc from '../loc';
import { isDesktop } from './environment';
@ -37,7 +36,7 @@ const _shareOpen = async (filePath: string, showShareDialog: boolean = false) =>
/**
* Writes a file to fs, and triggers an OS sharing dialog, so user can decide where to put this file (share to cloud
* or perhabs messaging app). Provided filename should be just a file name, NOT a path
* or perhaps messaging app). Provided filename should be just a file name, NOT a path
*/
export const writeFileAndExport = async function (fileName: string, contents: string, showShareDialog: boolean = true) {
@ -48,36 +47,21 @@ export const writeFileAndExport = async function (fileName: string, contents: st
await RNFS.writeFile(filePath, contents);
await _shareOpen(filePath, showShareDialog);
} else if (Platform.OS === 'android') {
const isAndroidVersion33OrAbove = Platform.Version >= 33;
const permissionType = isAndroidVersion33OrAbove ? PERMISSIONS.ANDROID.READ_MEDIA_IMAGES : PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE;
const result = await request(permissionType);
if (result === 'granted') {
const filePath = `${RNFS.ExternalDirectoryPath}/${sanitizedFileName}`;
try {
await RNFS.writeFile(filePath, contents);
if (showShareDialog) {
await _shareOpen(filePath);
} else {
presentAlert({ message: loc.formatString(loc.send.file_saved_at_path, { filePath }) });
}
} catch (e: any) {
presentAlert({ message: e.message });
const filePath = `${RNFS.DownloadDirectoryPath}/${sanitizedFileName}`;
try {
await RNFS.writeFile(filePath, contents);
if (showShareDialog) {
await _shareOpen(filePath);
} else {
presentAlert({ message: loc.formatString(loc.send.file_saved_at_path, { filePath }) });
}
} else {
Alert.alert(loc.send.permission_storage_title, loc.send.permission_storage_denied_message, [
{
text: loc.send.open_settings,
onPress: () => {
Linking.openSettings();
},
style: 'default',
},
{ text: loc._.cancel, onPress: () => {}, style: 'cancel' },
]);
} catch (e: any) {
console.error(e);
presentAlert({ message: e.message });
}
}
} catch (error: any) {
console.error(error);
presentAlert({ message: error.message });
}
};
@ -114,7 +98,7 @@ const _readPsbtFileIntoBase64 = async function (uri: string): Promise<string> {
} else {
// file was a text file, having base64 psbt in there. so we basically have double base64encoded string
// thats why we are returning string that was decoded once;
// most likely produced by Coldcard
// most likely produced by ColdCard
return stringData;
}
};
@ -183,23 +167,11 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri
if (res.fileCopyUri.toLowerCase().endsWith('.psbt')) {
// this is either binary file from ElectrumDesktop OR string file with base64 string in there
const file = await _readPsbtFileIntoBase64(fileCopyUri);
return { data: file, uri: decodeURI(res.fileCopyUri) };
return { data: file, uri: fileCopyUri };
}
if (res.type === DocumentPicker.types.images || res.type?.startsWith('image/')) {
try {
const uri2 = res.fileCopyUri.replace('file://', '');
const result = await RNQRGenerator.detect({ uri: decodeURI(uri2) });
if (result) {
return { data: result.values[0], uri: fileCopyUri };
}
presentAlert({ message: loc.send.qr_error_no_qrcode });
return { data: false, uri: false };
} catch (error) {
console.error(error);
presentAlert({ message: loc.send.qr_error_no_qrcode });
return { data: false, uri: false };
}
return await handleImageFile(fileCopyUri);
}
const file = await RNFS.readFile(fileCopyUri);
@ -212,6 +184,33 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri
}
};
const handleImageFile = async (fileCopyUri: string): Promise<{ data: string | false; uri: string | false }> => {
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 };
}
};
export const readFileOutsideSandbox = (filePath: string) => {
if (Platform.OS === 'ios') {
return readFile(filePath);

View file

@ -6,6 +6,7 @@ import { checkNotifications, requestNotifications, RESULTS } from 'react-native-
import PushNotification from 'react-native-push-notification';
import loc from '../loc';
import { groundControlUri } from './constants';
import { fetch } from '../util/fetch';
const PUSH_TOKEN = 'PUSH_TOKEN';
const GROUNDCONTROL_BASE_URI = 'GROUNDCONTROL_BASE_URI';
@ -385,10 +386,6 @@ export const configureNotifications = async onProcessNotifications => {
});
};
const _sleep = async ms => {
return new Promise(resolve => setTimeout(resolve, ms));
};
/**
* Validates whether the provided GroundControl URI is valid by pinging it.
*
@ -396,15 +393,13 @@ const _sleep = async ms => {
* @returns {Promise<boolean>} TRUE if valid, FALSE otherwise
*/
export const isGroundControlUriValid = async uri => {
let response;
try {
response = await Promise.race([fetch(`${uri}/ping`, { headers: _getHeaders() }), _sleep(2000)]);
} catch (_) {}
if (!response) return false;
const json = await response.json();
return !!json.description;
const response = await fetch(`${uri}/ping`, { headers: _getHeaders() });
const json = await response.json();
return !!json.description;
} catch (_) {
return false;
}
};
export const isNotificationsCapable = hasGmsSync() || hasHmsSync() || Platform.OS !== 'android';
@ -430,24 +425,21 @@ const getLevels = async () => {
const pushToken = await getPushToken();
if (!pushToken || !pushToken.token || !pushToken.os) return;
let response;
try {
response = await Promise.race([
fetch(`${baseURI}/getTokenConfiguration`, {
method: 'POST',
headers: _getHeaders(),
body: JSON.stringify({
token: pushToken.token,
os: pushToken.os,
}),
const response = await fetch(`${baseURI}/getTokenConfiguration`, {
method: 'POST',
headers: _getHeaders(),
body: JSON.stringify({
token: pushToken.token,
os: pushToken.os,
}),
_sleep(3000),
]);
} catch (_) {}
});
if (!response) return {};
return await response.json();
if (!response) return {};
return await response.json();
} catch (_) {
return {};
}
};
/**
@ -574,6 +566,9 @@ export const isNotificationsEnabled = async () => {
return !isDisabledByUser && !!token && !!levels.level_all;
} catch (error) {
console.log('Error checking notification levels:', error);
if (error instanceof SyntaxError) {
throw error;
}
return false;
}
};

View file

@ -157,7 +157,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
LIBRARY_SEARCH_PATHS = (
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
@ -209,7 +209,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
LIBRARY_SEARCH_PATHS = (
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",

View file

@ -1,4 +1,5 @@
import URL from 'url';
import { fetch } from '../util/fetch';
export default class Azteco {
/**

View file

@ -1,7 +1,6 @@
import bip21, { TOptions } from 'bip21';
import * as bitcoin from 'bitcoinjs-lib';
import URL from 'url';
import { readFileOutsideSandbox } from '../blue_modules/fs';
import { Chain } from '../models/bitcoinUnits';
import { WatchOnlyWallet } from './';
@ -87,9 +86,9 @@ class DeeplinkSchemaMatch {
} else if (wallet.chain === Chain.OFFCHAIN) {
if (action === 'openSend') {
completionHandler([
'ScanLndInvoiceRoot',
'ScanLNDInvoiceRoot',
{
screen: 'ScanLndInvoice',
screen: 'ScanLNDInvoice',
params: {
walletID: wallet.getID(),
},
@ -157,9 +156,9 @@ class DeeplinkSchemaMatch {
]);
} else if (DeeplinkSchemaMatch.isLightningInvoice(event.url)) {
completionHandler([
'ScanLndInvoiceRoot',
'ScanLNDInvoiceRoot',
{
screen: 'ScanLndInvoice',
screen: 'ScanLNDInvoice',
params: {
uri: event.url.replace('://', ':'),
},
@ -182,9 +181,9 @@ class DeeplinkSchemaMatch {
// this might be not just an email but a lightning address
// @see https://lightningaddress.com
completionHandler([
'ScanLndInvoiceRoot',
'ScanLNDInvoiceRoot',
{
screen: 'ScanLndInvoice',
screen: 'ScanLNDInvoice',
params: {
uri: event.url,
},
@ -306,9 +305,9 @@ class DeeplinkSchemaMatch {
];
} else {
return [
'ScanLndInvoiceRoot',
'ScanLNDInvoiceRoot',
{
screen: 'ScanLndInvoice',
screen: 'ScanLNDInvoice',
params: {
uri: uri.lndInvoice,
walletID: wallet.getID(),

View file

@ -6,6 +6,7 @@ import CryptoJS from 'crypto-js';
// @ts-ignore theres no types for secp256k1
import secp256k1 from 'secp256k1';
import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api
import { fetch } from '../util/fetch';
const ONION_REGEX = /^(http:\/\/[^/:@]+\.onion(?::\d{1,5})?)(\/.*)?$/; // regex for onion URL

View file

@ -310,8 +310,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// then we combine it all together
const addresses2fetch = [];
// Store these values to avoid a race condition if fetchBalance func changes them
const next_free_address_index = this.next_free_address_index;
const next_free_change_address_index = this.next_free_change_address_index;
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
for (let c = 0; c < next_free_address_index + this.gap_limit; c++) {
// external addresses first
let hasUnconfirmed = false;
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
@ -322,7 +325,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
for (let c = 0; c < next_free_change_address_index + this.gap_limit; c++) {
// next, internal addresses
let hasUnconfirmed = false;
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
@ -389,10 +392,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// now purge all unconfirmed txs from internal hashmaps, since some may be evicted from mempool because they became invalid
// or replaced. hashmaps are going to be re-populated anyways, since we fetched TXs for addresses with unconfirmed TXs
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
for (let c = 0; c < next_free_address_index + this.gap_limit; c++) {
this._txs_by_external_index[c] = this._txs_by_external_index[c].filter(tx => !!tx.confirmations);
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
for (let c = 0; c < next_free_change_address_index + this.gap_limit; c++) {
this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations);
}
for (const pc of this._receive_payment_codes) {
@ -404,7 +407,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// now, we need to put transactions in all relevant `cells` of internal hashmaps:
// this._txs_by_internal_index, this._txs_by_external_index & this._txs_by_payment_code_index
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
for (let c = 0; c < next_free_address_index + this.gap_limit; c++) {
for (const tx of Object.values(txdatas)) {
for (const vin of tx.vin) {
if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
@ -445,7 +448,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
for (let c = 0; c < next_free_change_address_index + this.gap_limit; c++) {
for (const tx of Object.values(txdatas)) {
for (const vin of tx.vin) {
if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {

View file

@ -1,5 +1,6 @@
import b58 from 'bs58check';
import createHash from 'create-hash';
import wif from 'wif';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { CreateTransactionResult, CreateTransactionUtxo, Transaction, Utxo } from './types';
@ -211,6 +212,17 @@ export class AbstractWallet {
setSecret(newSecret: string): this {
const origSecret = newSecret;
// is it minikey https://en.bitcoin.it/wiki/Mini_private_key_format
// Starts with S, is 22 length or larger, is base58
if (newSecret.startsWith('S') && newSecret.length >= 22 && /^[1-9A-HJ-NP-Za-km-z]+$/.test(newSecret)) {
// minikey + ? hashed with SHA256 starts with 0x00 byte
if (createHash('sha256').update(`${newSecret}?`).digest('hex').startsWith('00')) {
// it is a valid minikey
newSecret = wif.encode(0x80, createHash('sha256').update(newSecret).digest(), false);
}
}
this.secret = newSecret.trim().replace('bitcoin:', '').replace('BITCOIN:', '');
if (this.secret.startsWith('BC1')) this.secret = this.secret.toLowerCase();

View file

@ -1,6 +1,7 @@
import bolt11 from 'bolt11';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { LegacyWallet } from './legacy-wallet';
import { fetch } from '../../util/fetch';
export class LightningCustodianWallet extends LegacyWallet {
static readonly type = 'lightningCustodianWallet';

View file

@ -1090,9 +1090,8 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
}
getID() {
const string2hash = [...this._cosignersFingerprints].sort().join(',');
const id = createHash('sha256').update(string2hash).digest().toString('hex');
return id;
const string2hash = [...this._cosigners].sort().join(',') + ';' + [...this._cosignersFingerprints].sort().join(',');
return createHash('sha256').update(string2hash).digest().toString('hex');
}
calculateFeeFromPsbt(psbt: Psbt) {

View file

@ -79,6 +79,20 @@ export type TransactionOutput = {
};
};
export interface DecodedInvoice {
destination: string;
payment_hash: string;
num_satoshis: number;
timestamp: number;
expiry: number;
description: string;
description_hash: string;
fallback_addr: string;
cltv_expiry: string;
route_hints: any[];
[key: string]: any;
}
export type LightningTransaction = {
memo?: string;
type?: 'user_invoice' | 'payment_request' | 'bitcoind_tx' | 'paid_invoice';

View file

@ -5,7 +5,7 @@ import { useTheme } from './themes';
import ToolTipMenu from './TooltipMenu';
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
import loc from '../loc';
import { navigationRef } from '../NavigationService';
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
type AddWalletButtonProps = {
onPress?: (event: GestureResponderEvent) => void;
@ -23,21 +23,25 @@ const styles = StyleSheet.create({
const AddWalletButton: React.FC<AddWalletButtonProps> = ({ onPress }) => {
const { colors } = useTheme();
const navigation = useExtendedNavigation();
const stylesHook = StyleSheet.create({
ball: {
backgroundColor: colors.buttonBackgroundColor,
},
});
const onPressMenuItem = useCallback((action: string) => {
switch (action) {
case CommonToolTipActions.ImportWallet.id:
navigationRef.current?.navigate('AddWalletRoot', { screen: 'ImportWallet' });
break;
default:
break;
}
}, []);
const onPressMenuItem = useCallback(
(action: string) => {
switch (action) {
case CommonToolTipActions.ImportWallet.id:
navigation.navigate('AddWalletRoot', { screen: 'ImportWallet' });
break;
default:
break;
}
},
[navigation],
);
const actions = useMemo(() => [CommonToolTipActions.ImportWallet], []);

View file

@ -1,23 +1,18 @@
import React, { useCallback } from 'react';
import { Keyboard, StyleProp, StyleSheet, TextInput, View, ViewStyle } from 'react-native';
import React from 'react';
import { StyleProp, StyleSheet, TextInput, View, ViewStyle } from 'react-native';
import loc from '../loc';
import { AddressInputScanButton } from './AddressInputScanButton';
import { useTheme } from './themes';
import DeeplinkSchemaMatch from '../class/deeplink-schema-match';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
interface AddressInputProps {
isLoading?: boolean;
address?: string;
placeholder?: string;
onChangeText: (text: string) => void;
onBarScanned: (ret: { data?: any }) => void;
scanButtonTapped?: () => void;
launchedBy?: string;
editable?: boolean;
inputAccessoryViewID?: string;
onBlur?: () => void;
onFocus?: () => void;
onBlur?: () => void;
testID?: string;
style?: StyleProp<ViewStyle>;
keyboardType?:
@ -42,13 +37,10 @@ const AddressInput = ({
testID = 'AddressInput',
placeholder = loc.send.details_address,
onChangeText,
onBarScanned,
scanButtonTapped = () => {},
launchedBy,
editable = true,
inputAccessoryViewID,
onBlur = () => {},
onFocus = () => {},
onBlur = () => {},
keyboardType = 'default',
style,
}: AddressInputProps) => {
@ -64,24 +56,6 @@ const AddressInput = ({
},
});
const validateAddressWithFeedback = useCallback((value: string) => {
const isBitcoinAddress = DeeplinkSchemaMatch.isBitcoinAddress(value);
const isLightningInvoice = DeeplinkSchemaMatch.isLightningInvoice(value);
const isValid = isBitcoinAddress || isLightningInvoice;
triggerHapticFeedback(isValid ? HapticFeedbackTypes.NotificationSuccess : HapticFeedbackTypes.NotificationError);
return {
isValid,
type: isBitcoinAddress ? 'bitcoin' : isLightningInvoice ? 'lightning' : 'invalid',
};
}, []);
const onBlurEditing = () => {
validateAddressWithFeedback(address);
onBlur();
Keyboard.dismiss();
};
return (
<View style={[styles.root, stylesHook.root, style]}>
<TextInput
@ -95,21 +69,13 @@ const AddressInput = ({
multiline={!editable}
inputAccessoryViewID={inputAccessoryViewID}
clearButtonMode="while-editing"
onBlur={onBlurEditing}
onFocus={onFocus}
autoCapitalize="none"
autoCorrect={false}
keyboardType={keyboardType}
onBlur={onBlur}
/>
{editable ? (
<AddressInputScanButton
isLoading={isLoading}
launchedBy={launchedBy}
scanButtonTapped={scanButtonTapped}
onBarScanned={onBarScanned}
onChangeText={onChangeText}
/>
) : null}
{editable ? <AddressInputScanButton isLoading={isLoading} onChangeText={onChangeText} /> : null}
</View>
);
};

View file

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Image, Keyboard, Platform, StyleSheet, Text } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
import ToolTipMenu from './TooltipMenu';
@ -9,33 +9,27 @@ import { useTheme } from './themes';
import RNQRGenerator from 'rn-qr-generator';
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
import { useSettings } from '../hooks/context/useSettings';
import { useRoute } from '@react-navigation/native';
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
interface AddressInputScanButtonProps {
isLoading: boolean;
launchedBy?: string;
scanButtonTapped: () => void;
onBarScanned: (ret: { data?: any }) => void;
isLoading?: boolean;
onChangeText: (text: string) => void;
}
interface RouteParams {
onBarScanned?: any;
type?: 'default' | 'link';
testID?: string;
beforePress?: () => Promise<void> | void;
}
export const AddressInputScanButton = ({
isLoading,
launchedBy,
scanButtonTapped,
onBarScanned,
onChangeText,
type = 'default',
testID = 'BlueAddressInputScanQrButton',
beforePress,
}: AddressInputScanButtonProps) => {
const { colors } = useTheme();
const { isClipboardGetContentEnabled } = useSettings();
const navigation = useExtendedNavigation();
const params = useRoute().params as RouteParams;
const stylesHook = StyleSheet.create({
scan: {
backgroundColor: colors.scanLabel,
@ -46,16 +40,17 @@ export const AddressInputScanButton = ({
});
const toolTipOnPress = useCallback(async () => {
await scanButtonTapped();
if (beforePress) {
await beforePress();
}
Keyboard.dismiss();
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
}, [navigation, scanButtonTapped]);
}, [navigation, beforePress]);
const actions = useMemo(() => {
const availableActions = [
CommonToolTipActions.ScanQR,
CommonToolTipActions.ChoosePhoto,
CommonToolTipActions.ImportFile,
{
@ -67,20 +62,10 @@ export const AddressInputScanButton = ({
return availableActions;
}, [isClipboardGetContentEnabled]);
useEffect(() => {
const data = params.onBarScanned;
if (data) {
onBarScanned({ data });
navigation.setParams({ onBarScanned: undefined });
}
});
const onMenuItemPressed = useCallback(
async (action: string) => {
if (onBarScanned === undefined) throw new Error('onBarScanned is required');
switch (action) {
case CommonToolTipActions.ScanQR.id:
scanButtonTapped();
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
@ -147,7 +132,7 @@ export const AddressInputScanButton = ({
}
Keyboard.dismiss();
},
[navigation, onBarScanned, onChangeText, scanButtonTapped],
[navigation, onChangeText],
);
const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]);
@ -157,21 +142,29 @@ export const AddressInputScanButton = ({
actions={actions}
isButton
onPressMenuItem={onMenuItemPressed}
testID="BlueAddressInputScanQrButton"
testID={testID}
disabled={isLoading}
onPress={toolTipOnPress}
buttonStyle={buttonStyle}
buttonStyle={type === 'default' ? buttonStyle : undefined}
accessibilityLabel={loc.send.details_scan}
accessibilityHint={loc.send.details_scan_hint}
>
<Image source={require('../img/scan-white.png')} accessible={false} />
<Text style={[styles.scanText, stylesHook.scanText]} accessible={false}>
{loc.send.details_scan}
</Text>
{type === 'default' ? (
<>
<Image source={require('../img/scan-white.png')} accessible={false} />
<Text style={[styles.scanText, stylesHook.scanText]} accessible={false}>
{loc.send.details_scan}
</Text>
</>
) : (
<Text style={[styles.linkText, { color: colors.foregroundColor }]}>{loc.wallets.import_scan_qr}</Text>
)}
</ToolTipMenu>
);
};
AddressInputScanButton.displayName = 'AddressInputScanButton';
const styles = StyleSheet.create({
scan: {
height: 36,
@ -186,4 +179,8 @@ const styles = StyleSheet.create({
scanText: {
marginLeft: 4,
},
linkText: {
textAlign: 'center',
fontSize: 16,
},
});

View file

@ -146,7 +146,9 @@ class AmountInput extends Component {
textInput = React.createRef();
handleTextInputOnPress = () => {
this.textInput.current.focus();
if (this.textInput && this.textInput.current && typeof this.textInput.current.focus === 'function') {
this.textInput.current.focus();
}
};
handleChangeText = text => {
@ -258,7 +260,11 @@ class AmountInput extends Component {
accessibilityRole="button"
accessibilityLabel={loc._.enter_amount}
disabled={this.props.pointerEvents === 'none'}
onPress={() => this.textInput.focus()}
onPress={() => {
if (this.textInput && this.textInput.current && typeof this.textInput.current.focus === 'function') {
this.textInput.current.focus();
}
}}
>
<>
<View style={styles.root}>

View file

@ -1,5 +1,5 @@
import React, { forwardRef, useImperativeHandle, useRef, ReactElement, ComponentType } from 'react';
import { SheetSize, SizeInfo, TrueSheet, TrueSheetProps } from '@lodev09/react-native-true-sheet';
import { SheetSize, SizeChangeEvent, TrueSheet, TrueSheetProps } from '@lodev09/react-native-true-sheet';
import { Keyboard, Image, StyleSheet, View, TouchableOpacity, Platform, GestureResponderEvent, Text } from 'react-native';
import SaveFileButton from './SaveFileButton';
import { useTheme } from './themes';
@ -14,7 +14,7 @@ interface BottomModalProps extends TrueSheetProps {
footer?: ReactElement | ComponentType<any> | null;
footerDefaultMargins?: boolean | number;
onPresent?: () => void;
onSizeChange?: (size: SizeInfo) => void;
onSizeChange?: (event: SizeChangeEvent) => void;
showCloseButton?: boolean;
shareContent?: BottomModalShareContent;
shareButtonOnPress?: (event: GestureResponderEvent) => void;

View file

@ -1,10 +1,13 @@
import React, { useState, useRef } from 'react';
import { Animated, StyleSheet, TouchableOpacity, View } from 'react-native';
import { Animated, Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
// @ts-ignore: no declaration file yet
import { Camera, CameraApi, CameraType, Orientation } from 'react-native-camera-kit';
import loc from '../loc';
import { Icon } from '@rneui/base';
import { OnOrientationChangeData, OnReadCodeData } from 'react-native-camera-kit/dist/CameraProps';
import { triggerSelectionHapticFeedback } from '../blue_modules/hapticFeedback';
import { isDesktop } from '../blue_modules/environment';
// @ts-ignore: no declaration file yet
import { OnOrientationChangeData, OnReadCodeData } from 'react-native-camera-kit/dist/CameraProps';
interface CameraScreenProps {
onCancelButtonPress: () => void;
@ -46,8 +49,8 @@ const CameraScreen: React.FC<CameraScreenProps> = ({
// For real phone apps, lock your UI orientation using a library like 'react-native-orientation-locker'
const rotateUi = true;
const uiRotation = orientationAnim.interpolate({
inputRange: [1, 4],
outputRange: ['180deg', '-90deg'],
inputRange: [1, 2, 3, 4],
outputRange: ['180deg', '90deg', '0deg', '-90deg'],
});
const uiRotationStyle = rotateUi ? { transform: [{ rotate: uiRotation }] } : {};
@ -95,68 +98,104 @@ const CameraScreen: React.FC<CameraScreenProps> = ({
return (
<View style={styles.screen}>
<View style={styles.topButtons}>
<TouchableOpacity style={styles.topButton} onPress={onSetTorch}>
<Animated.View style={[styles.topButtonImg, uiRotationStyle]}>
<Icon name={torchMode ? 'flashlight-on' : 'flashlight-off'} type="font-awesome-6" color="#ffffff" />
</Animated.View>
</TouchableOpacity>
<View style={styles.rightButtonsContainer}>
{showImagePickerButton && (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.pick_image}
style={[styles.topButton, styles.spacing, uiRotationStyle]}
onPress={onImagePickerButtonPress}
>
<Animated.View style={[styles.topButtonImg, uiRotationStyle]}>
<Icon name="image" type="font-awesome" color="#ffffff" />
</Animated.View>
</TouchableOpacity>
)}
{showFilePickerButton && (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.pick_file}
style={[styles.topButton, styles.spacing, uiRotationStyle]}
onPress={onFilePickerButtonPress}
>
<Animated.View style={[styles.topButtonImg, uiRotationStyle]}>
<Icon name="file-import" type="font-awesome-5" color="#ffffff" />
</Animated.View>
</TouchableOpacity>
)}
{/* Render top buttons only if not desktop as they would not be relevant */}
{!isDesktop && (
<View style={styles.topButtons}>
<TouchableOpacity style={[styles.topButton, uiRotationStyle, torchMode ? styles.activeTorch : {}]} onPress={onSetTorch}>
<Animated.View style={styles.topButtonImg}>
{Platform.OS === 'ios' ? (
<Icon name={torchMode ? 'flashlight-on' : 'flashlight-off'} type="font-awesome-6" color={torchMode ? '#000' : '#fff'} />
) : (
<Icon name={torchMode ? 'flash-on' : 'flash-off'} type="ionicons" color={torchMode ? '#000' : '#fff'} />
)}
</Animated.View>
</TouchableOpacity>
<View style={styles.rightButtonsContainer}>
{showImagePickerButton && (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.pick_image}
style={[styles.topButton, styles.spacing, uiRotationStyle]}
onPress={onImagePickerButtonPress}
>
<Animated.View style={styles.topButtonImg}>
<Icon name="image" type="font-awesome" color="#ffffff" />
</Animated.View>
</TouchableOpacity>
)}
{showFilePickerButton && (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.pick_file}
style={[styles.topButton, styles.spacing, uiRotationStyle]}
onPress={onFilePickerButtonPress}
>
<Animated.View style={styles.topButtonImg}>
<Icon name="file-import" type="font-awesome-5" color="#ffffff" />
</Animated.View>
</TouchableOpacity>
)}
</View>
</View>
</View>
)}
<View style={styles.cameraContainer}>
<Camera
ref={cameraRef}
style={styles.cameraPreview}
cameraType={cameraType}
resetFocusWhenMotionDetected
zoom={zoom}
maxZoom={10}
scanBarcode
resizeMode="cover"
onZoom={handleZoom}
onReadCode={handleReadCode}
torchMode={torchMode ? 'on' : 'off'}
shutterPhotoSound
maxPhotoQualityPrioritization="quality"
resetFocusWhenMotionDetected
zoom={zoom}
onZoom={handleZoom}
maxZoom={10}
onOrientationChange={handleOrientationChange}
/>
</View>
<View style={styles.bottomButtons}>
<TouchableOpacity onPress={onCancelButtonPress}>
<Animated.Text style={[styles.backTextStyle, uiRotationStyle]}>{loc._.cancel}</Animated.Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bottomButton} onPress={onSwitchCameraPressed}>
<Animated.View style={[styles.topButtonImg, uiRotationStyle]}>
<Icon name="cameraswitch" type="font-awesome-6" color="#ffffff" />
</Animated.View>
</TouchableOpacity>
{isDesktop ? (
<View style={styles.rightButtonsContainer}>
{showImagePickerButton && (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.pick_image}
style={[styles.bottomButton, styles.spacing, uiRotationStyle]}
onPress={onImagePickerButtonPress}
>
<Animated.View style={styles.topButtonImg}>
<Icon name="image" type="font-awesome" color="#ffffff" />
</Animated.View>
</TouchableOpacity>
)}
{showFilePickerButton && (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.pick_file}
style={[styles.bottomButton, styles.spacing, uiRotationStyle]}
onPress={onFilePickerButtonPress}
>
<Animated.View style={styles.topButtonImg}>
<Icon name="file-import" type="font-awesome-5" color="#ffffff" />
</Animated.View>
</TouchableOpacity>
)}
</View>
) : (
<TouchableOpacity style={[styles.bottomButton, uiRotationStyle]} onPress={onSwitchCameraPressed}>
<Animated.View style={[styles.topButtonImg, uiRotationStyle]}>
{Platform.OS === 'ios' ? (
<Icon name="cameraswitch" type="font-awesome-6" color="#ffffff" />
) : (
<Icon name={cameraType === CameraType.Back ? 'camera-rear' : 'camera-front'} type="ionicons" color="#ffffff" />
)}
</Animated.View>
</TouchableOpacity>
)}
</View>
</View>
);
@ -165,6 +204,9 @@ const CameraScreen: React.FC<CameraScreenProps> = ({
export default CameraScreen;
const styles = StyleSheet.create({
activeTorch: {
backgroundColor: '#fff',
},
screen: {
height: '100%',
backgroundColor: '#000000',

View file

@ -1,14 +1,15 @@
import React, { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { InteractionManager } from 'react-native';
import { InteractionManager, LayoutAnimation } from 'react-native';
import A from '../../blue_modules/analytics';
import { BlueApp as BlueAppClass, LegacyWallet, TCounterpartyMetadata, TTXMetadata, WatchOnlyWallet } from '../../class';
import type { TWallet } from '../../class/wallets/types';
import presentAlert from '../../components/Alert';
import loc from '../../loc';
import loc, { formatBalanceWithoutSuffix } from '../../loc';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { startAndDecrypt } from '../../blue_modules/start-and-decrypt';
import { majorTomToGroundControl } from '../../blue_modules/notifications';
import { isNotificationsEnabled, majorTomToGroundControl, unsubscribe } from '../../blue_modules/notifications';
import { BitcoinUnit } from '../../models/bitcoinUnits';
const BlueApp = BlueAppClass.getInstance();
@ -49,6 +50,8 @@ interface StorageContextType {
cachedPassword: typeof BlueApp.cachedPassword;
getItem: typeof BlueApp.getItem;
setItem: typeof BlueApp.setItem;
handleWalletDeletion: (walletID: string, forceDelete?: boolean) => Promise<boolean>;
confirmWalletDeletion: (wallet: any, onConfirmed: () => void) => void;
}
export enum WalletTransactionsStatus {
@ -99,6 +102,120 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
setWallets([...BlueApp.getWallets()]);
}, []);
const handleWalletDeletion = useCallback(
async (walletID: string, forceDelete = false): Promise<boolean> => {
console.debug(`handleWalletDeletion: invoked for walletID ${walletID}`);
const wallet = wallets.find(w => w.getID() === walletID);
if (!wallet) {
console.warn(`handleWalletDeletion: wallet not found for ${walletID}`);
return false;
}
if (forceDelete) {
deleteWallet(wallet);
await saveToDisk(true);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
return true;
}
let isNotificationsSettingsEnabled = false;
try {
isNotificationsSettingsEnabled = await isNotificationsEnabled();
} catch (error) {
console.error(`handleWalletDeletion: error checking notifications for wallet ${walletID}`, error);
return await new Promise<boolean>(resolve => {
presentAlert({
title: loc.errors.error,
message: loc.wallets.details_delete_wallet_error_message,
buttons: [
{
text: loc.wallets.details_delete_anyway,
onPress: async () => {
const result = await handleWalletDeletion(walletID, true);
resolve(result);
},
style: 'destructive',
},
{
text: loc.wallets.list_tryagain,
onPress: async () => {
const result = await handleWalletDeletion(walletID);
resolve(result);
},
},
{
text: loc._.cancel,
onPress: () => resolve(false),
style: 'cancel',
},
],
options: { cancelable: false },
});
});
}
try {
if (isNotificationsSettingsEnabled) {
const externalAddresses = wallet.getAllExternalAddresses();
if (externalAddresses.length > 0) {
console.debug(`handleWalletDeletion: unsubscribing addresses for wallet ${walletID}`);
try {
await unsubscribe(externalAddresses, [], []);
console.debug(`handleWalletDeletion: unsubscribe succeeded for wallet ${walletID}`);
} catch (unsubscribeError) {
console.error(`handleWalletDeletion: unsubscribe failed for wallet ${walletID}`, unsubscribeError);
presentAlert({
title: loc.errors.error,
message: loc.wallets.details_delete_wallet_error_message,
buttons: [{ text: loc._.ok, onPress: () => {} }],
options: { cancelable: false },
});
return false;
}
}
}
deleteWallet(wallet);
console.debug(`handleWalletDeletion: wallet ${walletID} deleted successfully`);
await saveToDisk(true);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
return true;
} catch (e: unknown) {
console.error(`handleWalletDeletion: encountered error for wallet ${walletID}`, e);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
return await new Promise<boolean>(resolve => {
presentAlert({
title: loc.errors.error,
message: loc.wallets.details_delete_wallet_error_message,
buttons: [
{
text: loc.wallets.details_delete_anyway,
onPress: async () => {
const result = await handleWalletDeletion(walletID, true);
resolve(result);
},
style: 'destructive',
},
{
text: loc.wallets.list_tryagain,
onPress: async () => {
const result = await handleWalletDeletion(walletID);
resolve(result);
},
},
{
text: loc._.cancel,
onPress: () => resolve(false),
style: 'cancel',
},
],
options: { cancelable: false },
});
});
}
},
[deleteWallet, saveToDisk, wallets],
);
const resetWallets = useCallback(() => {
setWallets(BlueApp.getWallets());
}, []);
@ -120,56 +237,72 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
}
}, [walletsInitialized]);
// Add a refresh lock to prevent concurrent refreshes
const refreshingRef = useRef<boolean>(false);
const refreshAllWalletTransactions = useCallback(
async (lastSnappedTo?: number, showUpdateStatusIndicator: boolean = true) => {
const TIMEOUT_DURATION = 30000;
if (refreshingRef.current) {
console.debug('[refreshAllWalletTransactions] Refresh already in progress');
return;
}
refreshingRef.current = true;
await new Promise<void>(resolve => InteractionManager.runAfterInteractions(() => resolve()));
const TIMEOUT_DURATION = 30000;
const timeoutPromise = new Promise<never>((_resolve, reject) =>
setTimeout(() => {
reject(new Error('refreshAllWalletTransactions: Timeout reached'));
console.debug('[refreshAllWalletTransactions] Timeout reached');
reject(new Error('Timeout reached'));
}, TIMEOUT_DURATION),
);
const mainLogicPromise = new Promise<void>((resolve, reject) => {
InteractionManager.runAfterInteractions(async () => {
let noErr = true;
try {
await BlueElectrum.waitTillConnected();
if (showUpdateStatusIndicator) {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
}
const paymentCodesStart = Date.now();
await BlueApp.fetchSenderPaymentCodes(lastSnappedTo);
const paymentCodesEnd = Date.now();
console.debug('fetch payment codes took', (paymentCodesEnd - paymentCodesStart) / 1000, 'sec');
try {
if (showUpdateStatusIndicator) {
console.debug('[refreshAllWalletTransactions] Setting wallet transaction status to ALL');
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
}
console.debug('[refreshAllWalletTransactions] Waiting for connectivity...');
await BlueElectrum.waitTillConnected();
console.debug('[refreshAllWalletTransactions] Connected to Electrum');
// Restore fetch payment codes timing measurement
if (typeof BlueApp.fetchSenderPaymentCodes === 'function') {
const codesStart = Date.now();
console.debug('[refreshAllWalletTransactions] Fetching sender payment codes');
await BlueApp.fetchSenderPaymentCodes(lastSnappedTo);
const codesEnd = Date.now();
console.debug('[refreshAllWalletTransactions] fetch payment codes took', (codesEnd - codesStart) / 1000, 'sec');
} else {
console.warn('[refreshAllWalletTransactions] fetchSenderPaymentCodes is not available');
}
console.debug('[refreshAllWalletTransactions] Fetching wallet balances and transactions');
await Promise.race([
(async () => {
const balanceStart = Date.now();
await BlueApp.fetchWalletBalances(lastSnappedTo);
const balanceEnd = Date.now();
console.debug('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
console.debug('[refreshAllWalletTransactions] fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
const start = Date.now();
const txStart = Date.now();
await BlueApp.fetchWalletTransactions(lastSnappedTo);
const end = Date.now();
console.debug('fetch tx took', (end - start) / 1000, 'sec');
} catch (err) {
noErr = false;
console.error(err);
reject(err);
} finally {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
}
if (noErr) await saveToDisk();
resolve();
});
});
const txEnd = Date.now();
console.debug('[refreshAllWalletTransactions] fetch tx took', (txEnd - txStart) / 1000, 'sec');
try {
await Promise.race([mainLogicPromise, timeoutPromise]);
} catch (err) {
console.error('Error in refreshAllWalletTransactions:', err);
console.debug('[refreshAllWalletTransactions] Saving data to disk');
await saveToDisk();
})(),
timeoutPromise,
]);
console.debug('[refreshAllWalletTransactions] Refresh completed successfully');
} catch (error) {
console.error('[refreshAllWalletTransactions] Error:', error);
} finally {
console.debug('[refreshAllWalletTransactions] Resetting wallet transaction status and refresh lock');
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
refreshingRef.current = false;
}
},
[saveToDisk],
@ -182,24 +315,26 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
let noErr = true;
try {
if (Date.now() - (_lastTimeTriedToRefetchWallet[walletID] || 0) < 5000) {
console.debug('Re-fetch wallet happens too fast; NOP');
console.debug('[fetchAndSaveWalletTransactions] Re-fetch wallet happens too fast; NOP');
return;
}
_lastTimeTriedToRefetchWallet[walletID] = Date.now();
await BlueElectrum.waitTillConnected();
setWalletTransactionUpdateStatus(walletID);
const balanceStart = Date.now();
await BlueApp.fetchWalletBalances(index);
const balanceEnd = Date.now();
console.debug('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
const start = Date.now();
console.debug('[fetchAndSaveWalletTransactions] fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
const txStart = Date.now();
await BlueApp.fetchWalletTransactions(index);
const end = Date.now();
console.debug('fetch tx took', (end - start) / 1000, 'sec');
const txEnd = Date.now();
console.debug('[fetchAndSaveWalletTransactions] fetch tx took', (txEnd - txStart) / 1000, 'sec');
} catch (err) {
noErr = false;
console.error(err);
console.error('[fetchAndSaveWalletTransactions] Error:', err);
} finally {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
}
@ -217,10 +352,10 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
return;
}
const emptyWalletLabel = new LegacyWallet().getLabel();
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
if (w.getLabel() === emptyWalletLabel) w.setLabel(loc.wallets.import_imported + ' ' + w.typeReadable);
w.setUserHasSavedExport(true);
addWallet(w);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
await saveToDisk();
A(A.ENUM.CREATED_WALLET);
presentAlert({
@ -239,6 +374,36 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
[wallets, addWallet, saveToDisk],
);
function confirmWalletDeletion(wallet: any, onConfirmed: () => void) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning);
try {
const balance = formatBalanceWithoutSuffix(wallet.getBalance(), BitcoinUnit.SATS, true);
presentAlert({
title: loc.wallets.details_delete_wallet,
message: loc.formatString(loc.wallets.details_del_wb_q, { balance }),
buttons: [
{
text: loc.wallets.details_delete,
onPress: () => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
onConfirmed();
},
style: 'destructive',
},
{
text: loc._.cancel,
onPress: () => {},
style: 'cancel',
},
],
options: { cancelable: false },
});
} catch (error) {
// Handle error silently if needed
}
}
const value: StorageContextType = useMemo(
() => ({
wallets,
@ -274,6 +439,8 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
isPasswordInUse: BlueApp.isPasswordInUse,
walletTransactionUpdateStatus,
setWalletTransactionUpdateStatus,
handleWalletDeletion,
confirmWalletDeletion,
}),
[
wallets,
@ -291,7 +458,7 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
refreshAllWalletTransactions,
resetWallets,
walletTransactionUpdateStatus,
setWalletTransactionUpdateStatus,
handleWalletDeletion,
],
);

View file

@ -1,14 +1,18 @@
import React, { forwardRef, ReactNode, useEffect, useRef, useState } from 'react';
import React, { forwardRef, ReactNode, useEffect, useRef, useState, useCallback } from 'react';
import { Animated, Dimensions, PixelRatio, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from './themes';
const BORDER_RADIUS = 8;
const PADDINGS = 24;
const ICON_MARGIN = 7;
const cStyles = StyleSheet.create({
const buttonFontSize = (() => {
const baseSize = PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
return Math.min(22, baseSize);
})();
const containerStyles = StyleSheet.create({
root: {
alignSelf: 'center',
height: '6.9%',
@ -26,6 +30,27 @@ const cStyles = StyleSheet.create({
flexDirection: 'row',
overflow: 'hidden',
},
rootRound: {
borderRadius: 9999,
},
});
const buttonStyles = StyleSheet.create({
root: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
icon: {
alignItems: 'center',
},
text: {
fontSize: buttonFontSize,
fontWeight: '600',
marginLeft: ICON_MARGIN,
backgroundColor: 'transparent',
},
});
interface FContainerProps {
@ -51,93 +76,79 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
}).start();
}, [height, slideAnimation]);
const computeNewWidth = useCallback(
(layoutWidth: number, totalChildren: number) => {
const maxWidth = width - BORDER_RADIUS - 140;
const paddedWidth = Math.ceil(layoutWidth + PADDINGS * 2);
let calculatedWidth = paddedWidth * totalChildren > maxWidth ? Math.floor(maxWidth / totalChildren) : paddedWidth;
if (totalChildren === 1 && calculatedWidth < 90) calculatedWidth = 90;
return calculatedWidth;
},
[width],
);
const onLayout = (event: { nativeEvent: { layout: { width: number } } }) => {
if (layoutCalculated.current) return;
const maxWidth = width - BORDER_RADIUS - 140;
const layoutWidth = event.nativeEvent.layout.width;
const withPaddings = Math.ceil(layoutWidth + PADDINGS * 2);
const len = React.Children.toArray(props.children).filter(Boolean).length;
let newW = withPaddings * len > maxWidth ? Math.floor(maxWidth / len) : withPaddings;
if (len === 1 && newW < 90) newW = 90;
setNewWidth(newW);
const { width: layoutWidth } = event.nativeEvent.layout;
const totalChildren = React.Children.toArray(props.children).filter(Boolean).length;
setNewWidth(computeNewWidth(layoutWidth, totalChildren));
layoutCalculated.current = true;
};
const renderChild = (child: ReactNode, index: number, array: ReactNode[]): ReactNode => {
if (typeof child === 'string') {
return (
<View key={index} style={{ width: newWidth }}>
<Text adjustsFontSizeToFit numberOfLines={1}>
{child}
</Text>
</View>
);
}
return React.cloneElement(child as React.ReactElement<any>, {
width: newWidth,
key: index,
first: index === 0,
last: index === array.length - 1,
singleChild: array.length === 1,
});
};
const totalChildren = React.Children.toArray(props.children).filter(Boolean).length;
return (
<Animated.View
ref={ref}
onLayout={onLayout}
style={[
cStyles.root,
props.inline ? cStyles.rootInline : cStyles.rootAbsolute,
containerStyles.root,
props.inline ? containerStyles.rootInline : containerStyles.rootAbsolute,
bottomInsets,
newWidth ? cStyles.rootPost : cStyles.rootPre,
newWidth ? containerStyles.rootPost : containerStyles.rootPre,
totalChildren === 1 ? containerStyles.rootRound : null,
{ transform: [{ translateY: slideAnimation }] },
]}
>
{newWidth
? React.Children.toArray(props.children)
.filter(Boolean)
.map((child, index, array) => {
if (typeof child === 'string') {
return (
<View key={index} style={{ width: newWidth }}>
<Text adjustsFontSizeToFit numberOfLines={1}>
{child}
</Text>
</View>
);
}
return React.cloneElement(child as React.ReactElement<any>, {
width: newWidth,
key: index,
first: index === 0,
last: index === array.length - 1,
});
})
: props.children}
{newWidth ? React.Children.toArray(props.children).filter(Boolean).map(renderChild) : props.children}
</Animated.View>
);
});
const buttonFontSize =
PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22
? 22
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
const bStyles = StyleSheet.create({
root: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
icon: {
alignItems: 'center',
},
text: {
fontSize: buttonFontSize,
fontWeight: '600',
marginLeft: ICON_MARGIN,
backgroundColor: 'transparent',
},
});
interface FButtonProps {
text: string;
icon: ReactNode;
width?: number;
first?: boolean;
last?: boolean;
singleChild?: boolean;
disabled?: boolean;
testID?: string;
onPress: () => void;
onLongPress?: () => void;
}
export const FButton = ({ text, icon, width, first, last, testID, ...props }: FButtonProps) => {
export const FButton = ({ text, icon, width, first, last, singleChild, testID, ...props }: FButtonProps) => {
const { colors } = useTheme();
const bStylesHook = StyleSheet.create({
const customButtonStyles = StyleSheet.create({
root: {
backgroundColor: colors.buttonBackgroundColor,
borderRadius: BORDER_RADIUS,
@ -151,9 +162,12 @@ export const FButton = ({ text, icon, width, first, last, testID, ...props }: FB
marginRight: {
marginRight: 10,
},
rootRound: {
borderRadius: 9999,
},
});
const style: Record<string, any> = {};
const additionalStyles = !last ? bStylesHook.marginRight : {};
const additionalStyles = !last ? customButtonStyles.marginRight : {};
if (width) {
style.paddingHorizontal = PADDINGS;
@ -165,11 +179,15 @@ export const FButton = ({ text, icon, width, first, last, testID, ...props }: FB
accessibilityLabel={text}
accessibilityRole="button"
testID={testID}
style={[bStyles.root, bStylesHook.root, style, additionalStyles]}
style={[buttonStyles.root, customButtonStyles.root, style, additionalStyles, singleChild ? customButtonStyles.rootRound : null]}
{...props}
>
<View style={bStyles.icon}>{icon}</View>
<Text numberOfLines={1} adjustsFontSizeToFit style={[bStyles.text, props.disabled ? bStylesHook.textDisabled : bStylesHook.text]}>
<View style={buttonStyles.icon}>{icon}</View>
<Text
numberOfLines={1}
adjustsFontSizeToFit
style={[buttonStyles.text, props.disabled ? customButtonStyles.textDisabled : customButtonStyles.text]}
>
{text}
</Text>
</TouchableOpacity>

View file

@ -9,7 +9,12 @@ import { HandOffComponentProps } from './types';
const HandOffComponent: React.FC<HandOffComponentProps> = props => {
const { isHandOffUseEnabled } = useSettings();
console.debug('HandOffComponent is rendering.');
if (!props || !props.type || !props.userInfo || Object.keys(props.userInfo).length === 0) {
console.debug('HandOffComponent: Missing required type or userInfo data');
return null;
}
const userInfo = JSON.stringify(props.userInfo);
console.debug(`HandOffComponent is rendering. Type: ${props.type}, UserInfo: ${userInfo}...`);
return isHandOffUseEnabled ? <Handoff {...props} /> : null;
};

View file

@ -1,13 +1,13 @@
import React, { useCallback, useState } from 'react';
import { View, StyleSheet, ViewStyle, TouchableOpacity, ActivityIndicator } from 'react-native';
import React, { useCallback, useState, useEffect, useRef } from 'react';
import { StyleSheet, ViewStyle, TouchableOpacity, ActivityIndicator, Platform, Animated } from 'react-native';
import { Icon, ListItem } from '@rneui/base';
import { ExtendedTransaction, LightningTransaction, TWallet } from '../class/wallets/types';
import { WalletCarouselItem } from './WalletsCarousel';
import { TransactionListItem } from './TransactionListItem';
import { useTheme } from './themes';
import { BitcoinUnit } from '../models/bitcoinUnits';
import { TouchableOpacityWrapper } from './ListItem';
import loc from '../loc';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
enum ItemType {
WalletSection = 'wallet',
@ -40,6 +40,7 @@ interface ManageWalletsListItemProps {
handleToggleHideBalance: (wallet: TWallet) => void;
isActive?: boolean;
style?: ViewStyle;
globalDragActive?: boolean;
}
interface SwipeContentProps {
@ -66,7 +67,7 @@ const RightSwipeContent: React.FC<Partial<SwipeContentProps>> = ({ onPress }) =>
accessibilityRole="button"
accessibilityLabel="Delete Wallet"
>
<Icon name="delete-outline" color="#FFFFFF" />
<Icon name={Platform.OS === 'android' ? 'delete' : 'delete-outline'} color="#FFFFFF" />
</TouchableOpacity>
);
@ -83,10 +84,35 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
onPressIn,
onPressOut,
isActive,
globalDragActive,
style,
}) => {
const { colors } = useTheme();
const [isLoading, setIsLoading] = useState(false);
const [isSwipeActive, setIsSwipeActive] = useState(false);
const resetFunctionRef = useRef<(() => void) | null>(null);
const CARD_SORT_ACTIVE = 1.06;
const INACTIVE_SCALE_WHEN_ACTIVE = 0.9;
const SCALE_DURATION = 200;
const scaleValue = useRef(new Animated.Value(1)).current;
const prevIsActive = useRef(isActive);
const DEFAULT_VERTICAL_MARGIN = -10;
const REDUCED_VERTICAL_MARGIN = -50;
useEffect(() => {
if (isActive !== prevIsActive.current) {
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
}
prevIsActive.current = isActive;
Animated.timing(scaleValue, {
toValue: isActive ? CARD_SORT_ACTIVE : globalDragActive ? INACTIVE_SCALE_WHEN_ACTIVE : 1,
duration: SCALE_DURATION,
useNativeDriver: true,
}).start();
}, [isActive, globalDragActive, scaleValue]);
const onPress = useCallback(() => {
if (item.type === ItemType.WalletSection) {
@ -101,43 +127,83 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
reset();
};
const leftContent = (reset: () => void) => (
<LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />
);
const handleRightPress = (reset: () => void) => {
handleDeleteWallet(item.data as TWallet);
reset();
const leftContent = (reset: () => void) => {
resetFunctionRef.current = reset;
return <LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />;
};
const rightContent = (reset: () => void) => <RightSwipeContent onPress={() => handleRightPress(reset)} />;
const handleRightPress = (reset: () => void) => {
reset();
setTimeout(() => {
handleDeleteWallet(item.data as TWallet);
}, 100); // short delay to allow swipe reset animation to complete
};
const rightContent = (reset: () => void) => {
resetFunctionRef.current = reset;
return <RightSwipeContent onPress={() => handleRightPress(reset)} />;
};
const startDrag = useCallback(() => {
if (isSwipeActive) {
return;
}
if (resetFunctionRef.current) {
resetFunctionRef.current();
}
scaleValue.setValue(CARD_SORT_ACTIVE);
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
if (drag) {
drag();
}
}, [CARD_SORT_ACTIVE, drag, scaleValue, isSwipeActive]);
if (isLoading) {
return <ActivityIndicator size="large" color={colors.brandingColor} />;
}
if (item.type === ItemType.WalletSection) {
const animatedStyle = {
transform: [{ scale: scaleValue }],
marginVertical: globalDragActive && !isActive ? REDUCED_VERTICAL_MARGIN : DEFAULT_VERTICAL_MARGIN,
};
const backgroundColor = isActive || globalDragActive ? colors.brandingColor : colors.background;
const swipeDisabled = isActive || globalDragActive;
return (
<ListItem.Swipeable
leftWidth={80}
rightWidth={90}
containerStyle={[{ backgroundColor: colors.background }, style]}
leftContent={leftContent}
rightContent={rightContent}
Component={TouchableOpacityWrapper}
onPressOut={onPressOut}
onPressIn={onPressIn}
style={isActive ? styles.activeItem : undefined}
>
<ListItem.Content
style={{
backgroundColor: colors.background,
<Animated.View style={animatedStyle}>
<ListItem.Swipeable
leftWidth={swipeDisabled ? 0 : 80}
rightWidth={swipeDisabled ? 0 : 90}
containerStyle={[style, { backgroundColor }, swipeDisabled ? styles.transparentBackground : {}]}
leftContent={swipeDisabled ? null : leftContent}
rightContent={swipeDisabled ? null : rightContent}
onPressOut={onPressOut}
minSlideWidth={swipeDisabled ? 0 : 80}
onPressIn={onPressIn}
style={swipeDisabled ? styles.transparentBackground : {}}
onSwipeBegin={direction => {
if (!swipeDisabled) {
console.debug(`Swipe began: ${direction}`);
setIsSwipeActive(true);
}
}}
onSwipeEnd={() => {
if (!swipeDisabled) {
console.debug('Swipe ended');
setIsSwipeActive(false);
}
}}
>
<View style={styles.walletCarouselItemContainer}>
<ListItem.Content>
<WalletCarouselItem
item={item.data}
handleLongPress={isDraggingDisabled ? undefined : drag}
handleLongPress={isDraggingDisabled || isSwipeActive ? undefined : startDrag}
onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
@ -145,10 +211,11 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
searchQuery={state.searchQuery}
isPlaceHolder={isPlaceHolder}
renderHighlightedText={renderHighlightedText}
customStyle={styles.carouselItem}
/>
</View>
</ListItem.Content>
</ListItem.Swipeable>
</ListItem.Content>
</ListItem.Swipeable>
</Animated.View>
);
} else if (item.type === ItemType.TransactionSection && item.data) {
const w = state.wallets.find(wallet => wallet.getTransactions().some((tx: ExtendedTransaction) => tx.hash === item.data.hash));
@ -169,22 +236,22 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
};
const styles = StyleSheet.create({
walletCarouselItemContainer: {
width: '100%',
},
leftButtonContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
carouselItem: {
width: '100%',
},
rightButtonContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'red',
},
activeItem: {
backgroundColor: 'rgba(0, 0, 0, 0.1)',
transparentBackground: {
backgroundColor: 'transparent',
},
});

View file

@ -1,23 +1,66 @@
import PropTypes from 'prop-types';
import React, { useRef } from 'react';
import { ActivityIndicator, findNodeHandle, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import {
ActivityIndicator,
findNodeHandle,
GestureResponderEvent,
StyleProp,
StyleSheet,
Text,
TouchableOpacity,
View,
ViewStyle,
} from 'react-native';
import { Icon } from '@rneui/themed';
import ActionSheet from '../screen/ActionSheet';
import ActionSheetOptions from '../screen/ActionSheet.common';
import { useTheme } from './themes';
export const MultipleStepsListItemDashType = Object.freeze({ none: 0, top: 1, bottom: 2, topAndBottom: 3 });
export const MultipleStepsListItemButtohType = Object.freeze({ partial: 0, full: 1 });
import { ActionSheetOptions } from '../screen/ActionSheet.common';
const MultipleStepsListItem = props => {
export enum MultipleStepsListItemDashType {
None = 0,
Top = 1,
Bottom = 2,
TopAndBottom = 3,
}
export enum MultipleStepsListItemButtonType {
Partial = 0,
Full = 1,
}
interface MultipleStepsListItemProps {
circledText?: string;
checked?: boolean;
leftText?: string;
showActivityIndicator?: boolean;
isActionSheet?: boolean;
actionSheetOptions?: ActionSheetOptions;
dashes?: MultipleStepsListItemDashType;
button?: {
text?: string;
onPress?: (e: GestureResponderEvent | number) => void;
disabled?: boolean;
buttonType?: MultipleStepsListItemButtonType;
leftText?: string;
showActivityIndicator?: boolean;
testID?: string;
};
rightButton?: {
text?: string;
onPress?: () => void;
disabled?: boolean;
showActivityIndicator?: boolean;
};
}
const MultipleStepsListItem = (props: MultipleStepsListItemProps) => {
const { colors } = useTheme();
const {
showActivityIndicator = false,
dashes = MultipleStepsListItemDashType.none,
dashes = MultipleStepsListItemDashType.None,
circledText = '',
leftText = '',
checked = false,
useActionSheet = false,
isActionSheet = false,
actionSheetOptions = null, // Default to null or appropriate default
} = props;
const stylesHook = StyleSheet.create({
@ -43,7 +86,7 @@ const MultipleStepsListItem = props => {
const selfRef = useRef(null); // Create a ref for the component itself
const handleOnPressForActionSheet = () => {
if (useActionSheet && actionSheetOptions) {
if (isActionSheet && actionSheetOptions) {
// Clone options to modify them
let modifiedOptions = { ...actionSheetOptions };
@ -57,16 +100,16 @@ const MultipleStepsListItem = props => {
ActionSheet.showActionSheetWithOptions(modifiedOptions, buttonIndex => {
// Call the original onPress function, if provided, and not cancelled
if (buttonIndex !== -1 && props.button.onPress) {
if (buttonIndex !== -1 && props.button?.onPress) {
props.button.onPress(buttonIndex);
}
});
}
};
const renderDashes = () => {
const renderDashes = (): StyleProp<ViewStyle> => {
switch (dashes) {
case MultipleStepsListItemDashType.topAndBottom:
case MultipleStepsListItemDashType.TopAndBottom:
return {
width: 1,
borderStyle: 'dashed',
@ -77,7 +120,7 @@ const MultipleStepsListItem = props => {
marginLeft: 20,
position: 'absolute',
};
case MultipleStepsListItemDashType.bottom:
case MultipleStepsListItemDashType.Bottom:
return {
width: 1,
borderStyle: 'dashed',
@ -88,7 +131,7 @@ const MultipleStepsListItem = props => {
marginLeft: 20,
position: 'absolute',
};
case MultipleStepsListItemDashType.top:
case MultipleStepsListItemDashType.Top:
return {
width: 1,
borderStyle: 'dashed',
@ -105,6 +148,7 @@ const MultipleStepsListItem = props => {
};
const buttonOpacity = { opacity: props.button?.disabled ? 0.5 : 1.0 };
const rightButtonOpacity = { opacity: props.rightButton?.disabled ? 0.5 : 1.0 };
const onPress = isActionSheet ? handleOnPressForActionSheet : props.button?.onPress;
return (
<View>
<View style={renderDashes()} />
@ -131,19 +175,19 @@ const MultipleStepsListItem = props => {
{!showActivityIndicator && props.button && (
<>
{props.button.buttonType === undefined ||
(props.button.buttonType === MultipleStepsListItemButtohType.full && (
(props.button.buttonType === MultipleStepsListItemButtonType.Full && (
<TouchableOpacity
ref={useActionSheet ? selfRef : null}
ref={isActionSheet ? selfRef : null}
testID={props.button.testID}
accessibilityRole="button"
disabled={props.button.disabled}
style={[styles.provideKeyButton, stylesHook.provideKeyButton, buttonOpacity]}
onPress={useActionSheet ? handleOnPressForActionSheet : props.button.onPress}
onPress={onPress}
>
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.button.text}</Text>
</TouchableOpacity>
))}
{props.button.buttonType === MultipleStepsListItemButtohType.partial && (
{props.button.buttonType === MultipleStepsListItemButtonType.Partial && (
<View style={styles.buttonPartialContainer}>
<Text numberOfLines={1} style={[styles.rowPartialLeftText, stylesHook.rowPartialLeftText]} lineBreakMode="middle">
{props.button.leftText}
@ -153,7 +197,7 @@ const MultipleStepsListItem = props => {
accessibilityRole="button"
disabled={props.button.disabled}
style={[styles.rowPartialRightButton, stylesHook.provideKeyButton, rightButtonOpacity]}
onPress={props.button.onPress}
onPress={onPress}
>
{props.button.showActivityIndicator ? (
<ActivityIndicator />
@ -188,30 +232,6 @@ const MultipleStepsListItem = props => {
);
};
MultipleStepsListItem.propTypes = {
circledText: PropTypes.string,
checked: PropTypes.bool,
leftText: PropTypes.string,
showActivityIndicator: PropTypes.bool,
useActionSheet: PropTypes.bool,
actionSheetOptions: PropTypes.shape(ActionSheetOptions),
dashes: PropTypes.number,
button: PropTypes.shape({
text: PropTypes.string,
onPress: PropTypes.func,
disabled: PropTypes.bool,
buttonType: PropTypes.number,
leftText: PropTypes.string,
showActivityIndicator: PropTypes.bool,
}),
rightButton: PropTypes.shape({
text: PropTypes.string,
onPress: PropTypes.func,
disabled: PropTypes.bool,
showActivityIndicator: PropTypes.bool,
}),
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',

View file

@ -1,5 +1,5 @@
import React, { useState, useRef, forwardRef, useImperativeHandle, useEffect } from 'react';
import { View, Text, TextInput, StyleSheet, Animated, Easing, ViewStyle, Keyboard, Platform, UIManager, ScrollView } from 'react-native';
import { View, Text, TextInput, StyleSheet, Animated, Easing, ViewStyle, Keyboard, Platform, UIManager } from 'react-native';
import BottomModal, { BottomModalHandle } from './BottomModal';
import { useTheme } from '../components/themes';
import loc from '../loc';
@ -43,11 +43,10 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
const fadeInAnimation = useRef(new Animated.Value(0)).current;
const scaleAnimation = useRef(new Animated.Value(1)).current;
const shakeAnimation = useRef(new Animated.Value(0)).current;
const explanationOpacity = useRef(new Animated.Value(1)).current; // New animated value for opacity
const explanationOpacity = useRef(new Animated.Value(1)).current;
const { colors } = useTheme();
const passwordInputRef = useRef<TextInput>(null);
const confirmPasswordInputRef = useRef<TextInput>(null);
const scrollView = useRef<ScrollView>(null);
const { isVisible } = useKeyboard();
const stylesHook = StyleSheet.create({
@ -103,42 +102,43 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [modalType]);
const handleShakeAnimation = () => {
const performShake = (shakeAnimRef: Animated.Value) => {
Animated.sequence([
Animated.timing(shakeAnimation, {
Animated.timing(shakeAnimRef, {
toValue: 10,
duration: 100,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
Animated.timing(shakeAnimRef, {
toValue: -10,
duration: 100,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
Animated.timing(shakeAnimRef, {
toValue: 5,
duration: 100,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
Animated.timing(shakeAnimRef, {
toValue: -5,
duration: 100,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
Animated.timing(shakeAnimRef, {
toValue: 0,
duration: 100,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
]).start(() => {
confirmPasswordInputRef.current?.focus();
confirmPasswordInputRef.current?.setNativeProps({ selection: { start: 0, end: confirmPassword.length } });
});
]).start();
};
const handleShakeAnimation = () => {
performShake(shakeAnimation);
};
const handleSuccessAnimation = () => {
@ -180,6 +180,17 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
});
};
const handleConfirmationFailure = () => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
if (!isSuccess) handleShakeAnimation();
onConfirmationFailure();
};
const handleConfirmSuccess = () => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
handleSuccessAnimation();
};
const handleSubmit = async () => {
Keyboard.dismiss();
setIsLoading(true);
@ -189,37 +200,13 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
if (modalType === MODAL_TYPES.CREATE_PASSWORD || modalType === MODAL_TYPES.CREATE_FAKE_STORAGE) {
if (password === confirmPassword && password) {
success = await onConfirmationSuccess(password);
if (success) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
handleSuccessAnimation();
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
onConfirmationFailure();
if (!isSuccess) {
// Prevent shake animation if success is detected
handleShakeAnimation();
}
}
success ? handleConfirmSuccess() : handleConfirmationFailure();
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
if (!isSuccess) {
// Prevent shake animation if success is detected
handleShakeAnimation();
}
handleConfirmationFailure();
}
} else if (modalType === MODAL_TYPES.ENTER_PASSWORD) {
success = await onConfirmationSuccess(password);
if (success) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
handleSuccessAnimation();
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
if (!isSuccess) {
// Prevent shake animation if success is detected
handleShakeAnimation();
}
onConfirmationFailure();
}
success ? handleConfirmSuccess() : handleConfirmationFailure();
}
} finally {
setIsLoading(false); // Ensure loading state is reset
@ -258,18 +245,18 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
onConfirmationFailure();
};
const opacity = isVisible ? 0 : 1;
return (
<BottomModal
ref={modalRef}
onDismiss={onModalDismiss}
onClose={onModalDismiss}
grabber={false}
showCloseButton={!isSuccess}
onCloseModalPressed={handleCancel}
backgroundColor={colors.modal}
isGrabberVisible={!isSuccess}
scrollRef={scrollView}
keyboardMode="pan"
dismissible={false}
sizes={Platform.OS === 'ios' ? ['auto'] : [420, 'auto']}
footer={
!isSuccess ? (
showExplanation && modalType === MODAL_TYPES.CREATE_PASSWORD ? (
@ -282,16 +269,19 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
/>
</Animated.View>
) : (
<Animated.View style={[{ opacity: fadeOutAnimation, transform: [{ scale: scaleAnimation }] }, styles.feeModalFooter]}>
{!isVisible && (
<SecondButton
title={isLoading ? '' : loc._.ok}
onPress={handleSubmit}
testID="OKButton"
loading={isLoading}
disabled={isLoading || !password || (modalType === MODAL_TYPES.CREATE_PASSWORD && !confirmPassword)}
/>
)}
<Animated.View
style={[
{ opacity: isVisible ? opacity : fadeOutAnimation, transform: [{ scale: scaleAnimation }] },
styles.feeModalFooterSpacing,
]}
>
<SecondButton
title={isLoading ? '' : loc._.ok}
onPress={handleSubmit}
testID="OKButton"
loading={isLoading}
disabled={isLoading || !password || (modalType === MODAL_TYPES.CREATE_PASSWORD && !confirmPassword)}
/>
</Animated.View>
)
) : null
@ -302,14 +292,14 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
{modalType === MODAL_TYPES.CREATE_PASSWORD && showExplanation && (
<Animated.View style={{ opacity: explanationOpacity }}>
<Text style={[styles.textLabel, stylesHook.feeModalLabel]}>{loc.settings.encrypt_storage_explanation_headline}</Text>
<Animated.ScrollView style={styles.explanationScrollView} ref={scrollView}>
<Text style={[styles.description, stylesHook.feeModalCustomText]}>
<Animated.View>
<Text style={[styles.description, stylesHook.feeModalCustomText]} maxFontSizeMultiplier={1.2}>
{loc.settings.encrypt_storage_explanation_description_line1}
</Text>
<Text style={[styles.description, stylesHook.feeModalCustomText]}>
<Text style={[styles.description, stylesHook.feeModalCustomText]} maxFontSizeMultiplier={1.2}>
{loc.settings.encrypt_storage_explanation_description_line2}
</Text>
</Animated.ScrollView>
</Animated.View>
<View style={styles.feeModalFooter} />
</Animated.View>
)}
@ -398,29 +388,30 @@ export default PromptPasswordConfirmationModal;
const styles = StyleSheet.create({
modalContent: {
padding: 22,
width: '100%', // Ensure modal content takes full width
width: '100%',
justifyContent: 'center',
alignItems: 'center',
},
minHeight: {
minHeight: 280,
minHeight: 420,
},
feeModalFooter: {
padding: 16,
},
feeModalFooterSpacing: {
padding: 16,
padding: 24,
marginVertical: 24,
},
inputContainer: {
marginBottom: 10,
width: '100%', // Ensure full width
width: '100%',
},
input: {
borderRadius: 4,
padding: 8,
marginVertical: 8,
fontSize: 16,
width: '100%', // Ensure full width
width: '100%',
},
textLabel: {
fontSize: 20,
@ -436,7 +427,8 @@ const styles = StyleSheet.create({
successContainer: {
justifyContent: 'center',
alignItems: 'center',
height: 100,
margin: 24,
marginBottom: 48,
},
circle: {
width: 60,
@ -450,7 +442,4 @@ const styles = StyleSheet.create({
color: 'white',
fontSize: 30,
},
explanationScrollView: {
maxHeight: 200,
},
});

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import { requireNativeComponent, View, StyleSheet, NativeSyntheticEvent } from 'react-native';
interface SegmentedControlProps {
@ -21,9 +21,18 @@ interface NativeSegmentedControlProps {
const NativeSegmentedControl = requireNativeComponent<NativeSegmentedControlProps>('CustomSegmentedControl');
const SegmentedControl: React.FC<SegmentedControlProps> = ({ values, selectedIndex, onChange }) => {
const handleChange = (event: NativeSyntheticEvent<SegmentedControlEvent>) => {
onChange(event.nativeEvent.selectedIndex);
};
const handleChange = useMemo(
() => (event: NativeSyntheticEvent<SegmentedControlEvent>) => {
if (event?.nativeEvent?.selectedIndex !== undefined) {
onChange(event.nativeEvent.selectedIndex);
}
},
[onChange],
);
if (!Array.isArray(values) || values.length === 0) {
return null;
}
return (
<View style={styles.container}>

View file

@ -109,8 +109,8 @@ const SelectFeeModal = forwardRef<BottomModalHandle, SelectFeeModalProps>(
});
useImperativeHandle(ref, () => ({
present: async () => feeModalRef.current?.present(),
dismiss: async () => feeModalRef.current?.dismiss(),
present: async () => await feeModalRef.current?.present(),
dismiss: async () => await feeModalRef.current?.dismiss(),
}));
const options: Option[] = [
@ -163,8 +163,8 @@ const SelectFeeModal = forwardRef<BottomModalHandle, SelectFeeModalProps>(
const handleSelectOption = async (fee: number | null, rate: number) => {
setFeePrecalc(fp => ({ ...fp, current: fee }));
await feeModalRef.current?.dismiss();
setCustomFee(rate.toString());
await feeModalRef.current?.dismiss();
};
return (

73
components/TipBox.tsx Normal file
View file

@ -0,0 +1,73 @@
import React from 'react';
import { View, StyleSheet, ViewStyle } from 'react-native';
import { useTheme } from './themes';
import { BlueText } from '../BlueComponents';
interface TipBoxProps {
number?: string;
title?: string;
description?: string;
additionalDescription?: string;
containerStyle?: ViewStyle;
}
const TipBox: React.FC<TipBoxProps> = ({ number, title, description, additionalDescription, containerStyle }) => {
const { colors } = useTheme();
const stylesHook = StyleSheet.create({
tipBox: {
backgroundColor: colors.ballOutgoingExpired,
borderRadius: 12,
padding: 16,
marginBottom: 24,
...containerStyle,
},
tipHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: number || title ? 16 : 0,
},
tipHeaderText: {
marginLeft: 4,
flex: 1,
},
description: {
marginBottom: additionalDescription ? 16 : 0,
},
});
return (
<View style={stylesHook.tipBox}>
{(number || title) && (
<View style={stylesHook.tipHeader}>
{number && (
<View style={styles.vaultKeyCircle}>
<BlueText style={styles.vaultKeyText}>{number}</BlueText>
</View>
)}
{title && (
<BlueText bold style={stylesHook.tipHeaderText}>
{title}
</BlueText>
)}
</View>
)}
{description && <BlueText style={stylesHook.description}>{description}</BlueText>}
{additionalDescription && <BlueText>{additionalDescription}</BlueText>}
</View>
);
};
const styles = StyleSheet.create({
vaultKeyCircle: {
width: 32,
height: 32,
justifyContent: 'center',
alignItems: 'center',
},
vaultKeyText: {
fontSize: 18,
fontWeight: 'bold',
},
});
export default TipBox;

View file

@ -1,26 +1,15 @@
import React, { Ref, useCallback, useMemo } from 'react';
import { Platform, Pressable, TouchableOpacity } from 'react-native';
import React, { useCallback, useMemo } from 'react';
import { Platform, TouchableOpacity } from 'react-native';
import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu';
import {
ContextMenuView,
RenderItem,
OnPressMenuItemEventObject,
MenuState,
IconConfig,
MenuElementConfig,
} from 'react-native-ios-context-menu';
import { ToolTipMenuProps, Action } from './types';
import { useSettings } from '../hooks/context/useSettings';
const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
const ToolTipMenu = (props: ToolTipMenuProps) => {
const {
title = '',
isMenuPrimaryAction = false,
renderPreview,
disabled = false,
onPress,
onMenuWillShow,
onMenuWillHide,
buttonStyle,
onPressMenuItem,
children,
@ -30,58 +19,59 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
const { language } = useSettings();
// Map Menu Items for iOS Context Menu
const mapMenuItemForContextMenuView = useCallback((action: Action) => {
if (!action.id) return null;
return {
actionKey: action.id.toString(),
actionTitle: action.text,
icon: action.icon?.iconValue ? ({ iconType: 'SYSTEM', iconValue: action.icon.iconValue } as IconConfig) : undefined,
state: action.menuState ?? undefined,
attributes: action.disabled ? ['disabled'] : [],
};
}, []);
// Map Menu Items for RN Menu (supports subactions and displayInline)
const mapMenuItemForMenuView = useCallback((action: Action): MenuAction | null => {
if (!action.id) return null;
// Check for subactions
const subactions =
action.subactions?.map(subaction => ({
id: subaction.id.toString(),
title: subaction.text,
subtitle: subaction.subtitle,
image: subaction.icon?.iconValue ? subaction.icon.iconValue : undefined,
state: subaction.menuState === undefined ? undefined : ((subaction.menuState ? 'on' : 'off') as MenuState),
attributes: { disabled: subaction.disabled, destructive: subaction.destructive, hidden: subaction.hidden },
subactions: subaction.subactions?.map(subsubaction => ({
id: subsubaction.id.toString(),
title: subsubaction.text,
subtitle: subsubaction.subtitle,
image: subsubaction.icon?.iconValue ? subsubaction.icon.iconValue : undefined,
state: subsubaction.menuState === undefined ? undefined : ((subsubaction.menuState ? 'on' : 'off') as MenuState),
attributes: { disabled: subsubaction.disabled, destructive: subsubaction.destructive, hidden: subsubaction.hidden },
})),
})) || [];
action.subactions?.map(subaction => {
const subMenuItem: MenuAction = {
id: subaction.id.toString(),
title: subaction.text,
subtitle: subaction.subtitle,
image: subaction.icon?.iconValue ? subaction.icon.iconValue : undefined,
attributes: { disabled: subaction.disabled, destructive: subaction.destructive, hidden: subaction.hidden },
};
if ('menuState' in subaction) {
subMenuItem.state = subaction.menuState ? 'on' : 'off';
}
if (subaction.subactions && subaction.subactions.length > 0) {
const deepSubactions = subaction.subactions.map(deepSub => {
const deepMenuItem: MenuAction = {
id: deepSub.id.toString(),
title: deepSub.text,
subtitle: deepSub.subtitle,
image: deepSub.icon?.iconValue ? deepSub.icon.iconValue : undefined,
attributes: { disabled: deepSub.disabled, destructive: deepSub.destructive, hidden: deepSub.hidden },
};
if ('menuState' in deepSub) {
deepMenuItem.state = deepSub.menuState ? 'on' : 'off';
}
return deepMenuItem;
});
subMenuItem.subactions = deepSubactions;
}
return subMenuItem;
}) || [];
return {
const menuItem: MenuAction = {
id: action.id.toString(),
title: action.text,
subtitle: action.subtitle,
image: action.icon?.iconValue ? action.icon.iconValue : undefined,
state: action.menuState === undefined ? undefined : ((action.menuState ? 'on' : 'off') as MenuState),
attributes: { disabled: action.disabled, destructive: action.destructive, hidden: action.hidden },
subactions: subactions.length > 0 ? subactions : undefined,
displayInline: action.displayInline || false,
};
if ('menuState' in action) {
menuItem.state = action.menuState ? 'on' : 'off';
}
if (subactions.length > 0) {
menuItem.subactions = subactions;
}
return menuItem;
}, []);
const contextMenuItems = useMemo(() => {
const flattenedActions = props.actions.flat().filter(action => action.id);
return flattenedActions.map(mapMenuItemForContextMenuView).filter(item => item !== null) as MenuElementConfig[];
}, [props.actions, mapMenuItemForContextMenuView]);
const menuViewItemsIOS = useMemo(() => {
return props.actions
.map(actionGroup => {
@ -108,13 +98,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
return mergedActions.map(mapMenuItemForMenuView).filter(item => item !== null) as MenuAction[];
}, [props.actions, mapMenuItemForMenuView]);
const handlePressMenuItemForContextMenuView = useCallback(
(event: OnPressMenuItemEventObject) => {
onPressMenuItem(event.nativeEvent.actionKey);
},
[onPressMenuItem],
);
const handlePressMenuItemForMenuView = useCallback(
({ nativeEvent }: NativeActionEvent) => {
onPressMenuItem(nativeEvent.event);
@ -122,46 +105,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
[onPressMenuItem],
);
const renderContextMenuView = () => {
return (
<ContextMenuView
lazyPreview
accessibilityLabel={props.accessibilityLabel}
accessibilityHint={props.accessibilityHint}
accessibilityRole={props.accessibilityRole}
accessibilityState={props.accessibilityState}
accessibilityLanguage={language}
shouldEnableAggressiveCleanup
internalCleanupMode="automatic"
onPressMenuItem={handlePressMenuItemForContextMenuView}
onMenuWillShow={onMenuWillShow}
onMenuWillHide={onMenuWillHide}
useActionSheetFallback={false}
menuConfig={{
menuTitle: title,
menuItems: contextMenuItems,
}}
{...(renderPreview
? {
previewConfig: {
previewType: 'CUSTOM',
backgroundColor: 'white',
},
renderPreview: renderPreview as RenderItem,
}
: {})}
>
{onPress ? (
<Pressable accessibilityRole="button" onPress={onPress} {...restProps}>
{children}
</Pressable>
) : (
children
)}
</ContextMenuView>
);
};
const renderMenuView = () => {
return (
<MenuView
@ -187,7 +130,7 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
);
};
return props.actions.length > 0 ? (Platform.OS === 'ios' && renderPreview ? renderContextMenuView() : renderMenuView()) : null;
});
return props.actions.length > 0 ? renderMenuView() : null;
};
export default ToolTipMenu;

View file

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState, memo } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import Clipboard from '@react-native-clipboard/clipboard';
import { Linking, View, ViewStyle } from 'react-native';
@ -36,7 +36,7 @@ interface TransactionListItemProps {
type NavigationProps = NativeStackNavigationProp<DetailViewStackParamList>;
export const TransactionListItem: React.FC<TransactionListItemProps> = React.memo(
export const TransactionListItem: React.FC<TransactionListItemProps> = memo(
({ item, itemPriceUnit = BitcoinUnit.BTC, walletID, searchQuery, style, renderHighlightedText }) => {
const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1);
const { colors } = useTheme();
@ -46,10 +46,10 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
const { language, selectedBlockExplorer } = useSettings();
const containerStyle = useMemo(
() => ({
backgroundColor: 'transparent',
backgroundColor: colors.background,
borderBottomColor: colors.lightBorder,
}),
[colors.lightBorder],
[colors.background, colors.lightBorder],
);
const combinedStyle = useMemo(() => [containerStyle, style], [containerStyle, style]);
@ -81,28 +81,23 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
return sub || undefined;
}, [txMemo, item.confirmations, item.memo]);
const formattedAmount = useMemo(() => {
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
}, [item.value, itemPriceUnit]);
const rowTitle = useMemo(() => {
if (item.type === 'user_invoice' || item.type === 'payment_request') {
if (isNaN(Number(item.value))) {
item.value = 0;
}
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const now = Math.floor(currentDate.getTime() / 1000);
const invoiceExpiration = item.timestamp! + item.expire_time!;
if (invoiceExpiration > now) {
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
if (invoiceExpiration > now || item.ispaid) {
return formattedAmount;
} else {
if (item.ispaid) {
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
} else {
return loc.lnd.expired;
}
return loc.lnd.expired;
}
} else {
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
}
}, [item, itemPriceUnit]);
return formattedAmount;
}, [item, formattedAmount]);
const rowTitleStyle = useMemo(() => {
let color = colors.successColor;
@ -198,10 +193,9 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
const { label: transactionTypeLabel, icon: avatar } = determineTransactionTypeAndAvatar();
const amountWithUnit = useMemo(() => {
const amount = formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
const unit = itemPriceUnit === BitcoinUnit.BTC || itemPriceUnit === BitcoinUnit.SATS ? ` ${itemPriceUnit}` : ' ';
return `${amount}${unit}`;
}, [item.value, itemPriceUnit]);
const unitSuffix = itemPriceUnit === BitcoinUnit.BTC || itemPriceUnit === BitcoinUnit.SATS ? ` ${itemPriceUnit}` : ' ';
return `${formattedAmount}${unitSuffix}`;
}, [formattedAmount, itemPriceUnit]);
useEffect(() => {
setSubtitleNumberOfLines(1);
@ -226,7 +220,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
}
const loaded = await LN.loadSuccessfulPayment(paymentHash);
if (loaded) {
navigate('ScanLndInvoiceRoot', {
navigate('ScanLNDInvoiceRoot', {
screen: 'LnurlPaySuccess',
params: {
paymentHash,
@ -252,7 +246,19 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
setSubtitleNumberOfLines(0);
}, []);
const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]);
const handleOnDetailsPress = useCallback(() => {
if (walletID && item && item.hash) {
navigate('TransactionDetails', { tx: item, hash: item.hash, walletID });
} else {
const lightningWallet = wallets.find(wallet => wallet?.getID() === item.walletID);
if (lightningWallet) {
navigate('LNDViewInvoice', {
invoice: item,
walletID: lightningWallet.getID(),
});
}
}
}, [item, navigate, walletID, wallets]);
const handleOnCopyAmountTap = useCallback(() => Clipboard.setString(rowTitle.replace(/[\s\\-]/g, '')), [rowTitle]);
const handleOnCopyTransactionID = useCallback(() => Clipboard.setString(item.hash), [item.hash]);
@ -283,6 +289,8 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
handleCopyOpenInBlockExplorerPress();
} else if (id === CommonToolTipActions.CopyTXID.id) {
handleOnCopyTransactionID();
} else if (id === CommonToolTipActions.Details.id) {
handleOnDetailsPress();
}
},
[
@ -290,31 +298,40 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
handleOnCopyAmountTap,
handleOnCopyNote,
handleOnCopyTransactionID,
handleOnDetailsPress,
handleOnExpandNote,
handleOnViewOnBlockExplorer,
],
);
const toolTipActions = useMemo((): Action[] => {
const actions: (Action | Action[])[] = [];
if (rowTitle !== loc.lnd.expired) {
actions.push(CommonToolTipActions.CopyAmount);
}
if (subtitle) {
actions.push(CommonToolTipActions.CopyNote);
}
if (item.hash) {
actions.push(CommonToolTipActions.CopyTXID, CommonToolTipActions.CopyBlockExplorerLink, [CommonToolTipActions.OpenInBlockExplorer]);
}
if (subtitle && subtitleNumberOfLines === 1) {
actions.push([CommonToolTipActions.ExpandNote]);
}
const actions: (Action | Action[])[] = [
{
...CommonToolTipActions.CopyAmount,
hidden: rowTitle === loc.lnd.expired,
},
{
...CommonToolTipActions.CopyNote,
hidden: !subtitle,
},
{
...CommonToolTipActions.CopyTXID,
hidden: !item.hash,
},
{
...CommonToolTipActions.CopyBlockExplorerLink,
hidden: !item.hash,
},
[{ ...CommonToolTipActions.OpenInBlockExplorer, hidden: !item.hash }, CommonToolTipActions.Details],
[
{
...CommonToolTipActions.ExpandNote,
hidden: subtitleNumberOfLines !== 1,
},
],
];
return actions as Action[];
}, [item.hash, subtitle, rowTitle, subtitleNumberOfLines]);
}, [rowTitle, subtitle, item.hash, subtitleNumberOfLines]);
const accessibilityState = useMemo(() => {
return {
@ -322,6 +339,8 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
};
}, [subtitleNumberOfLines]);
const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]);
return (
<ToolTipMenu
isButton
@ -343,8 +362,17 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
rightTitle={rowTitle}
rightTitleStyle={rowTitleStyle}
containerStyle={combinedStyle}
testID="TransactionListItem"
/>
</ToolTipMenu>
);
},
(prevProps, nextProps) => {
return (
prevProps.item.hash === nextProps.item.hash &&
prevProps.item.received === nextProps.item.received &&
prevProps.itemPriceUnit === nextProps.itemPriceUnit &&
prevProps.walletID === nextProps.walletID
);
},
);

View file

@ -108,13 +108,14 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
];
}, []);
const balance = useMemo(() => {
const balanceFormatted =
unit === BitcoinUnit.LOCAL_CURRENCY
? formatBalance(wallet.getBalance(), unit, true)
: formatBalanceWithoutSuffix(wallet.getBalance(), unit, true);
return !hideBalance && balanceFormatted;
}, [unit, wallet, hideBalance]);
const currentBalance = wallet ? wallet.getBalance() : 0;
const formattedBalance = useMemo(() => {
return unit === BitcoinUnit.LOCAL_CURRENCY
? formatBalance(currentBalance, unit, true)
: formatBalanceWithoutSuffix(currentBalance, unit, true);
}, [unit, currentBalance]);
const balance = !wallet.hideBalance && formattedBalance;
const toolTipWalletBalanceActions = useMemo(() => {
return hideBalance

View file

@ -1,4 +1,4 @@
import React, { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react';
import {
Animated,
FlatList,
@ -98,8 +98,12 @@ interface WalletCarouselItemProps {
isSelectedWallet?: boolean;
customStyle?: ViewStyle;
horizontal?: boolean;
isPlaceHolder?: boolean;
searchQuery?: string;
renderHighlightedText?: (text: string, query: string) => JSX.Element;
animationsEnabled?: boolean;
onPressIn?: () => void;
onPressOut?: () => void;
}
const iStyles = StyleSheet.create({
@ -161,21 +165,6 @@ const iStyles = StyleSheet.create({
},
});
interface WalletCarouselItemProps {
item: TWallet;
onPress: (item: TWallet) => void;
handleLongPress?: () => void;
isSelectedWallet?: boolean;
customStyle?: ViewStyle;
horizontal?: boolean;
isPlaceHolder?: boolean;
searchQuery?: string;
renderHighlightedText?: (text: string, query: string) => JSX.Element;
animationsEnabled?: boolean;
onPressIn?: () => void;
onPressOut?: () => void;
}
export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
({
item,
@ -198,34 +187,31 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
const { isLargeScreen } = useIsLargeScreen();
const springConfig = useMemo(() => ({ useNativeDriver: true, tension: 100 }), []);
const animateScale = useCallback(
(toValue: number, callback?: () => void) => {
Animated.spring(scaleValue, { toValue, ...springConfig }).start(callback);
},
[scaleValue, springConfig],
);
const onPressedIn = useCallback(() => {
if (animationsEnabled) {
Animated.spring(scaleValue, {
toValue: 0.95,
useNativeDriver: true,
friction: 3,
tension: 100,
}).start();
animateScale(0.95);
}
if (onPressIn) onPressIn();
}, [scaleValue, animationsEnabled, onPressIn]);
}, [animateScale, animationsEnabled, onPressIn]);
const onPressedOut = useCallback(() => {
if (animationsEnabled) {
Animated.spring(scaleValue, {
toValue: 1.0,
useNativeDriver: true,
friction: 3,
tension: 100,
}).start();
animateScale(1.0);
}
if (onPressOut) onPressOut();
}, [scaleValue, animationsEnabled, onPressOut]);
}, [animateScale, animationsEnabled, onPressOut]);
const handlePress = useCallback(() => {
onPressedOut();
onPress(item);
}, [item, onPress, onPressedOut]);
}, [item, onPress]);
const opacity = isSelectedWallet === false ? 0.5 : 1.0;
let image;
@ -267,6 +253,8 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
if (handleLongPress) handleLongPress();
}}
onPress={handlePress}
delayHoverIn={0}
delayHoverOut={0}
>
<View style={[iStyles.shadowContainer, { backgroundColor: colors.background, shadowColor: colors.shadowColor }]}>
<LinearGradient colors={WalletGradient.gradientsFor(item.type)} style={iStyles.grad}>
@ -362,6 +350,10 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
renderHighlightedText,
isFlatList = true,
} = props;
const { width } = useWindowDimensions();
const itemWidth = React.useMemo(() => (width * 0.82 > 375 ? 375 : width * 0.82), [width]);
const renderItem = useCallback(
({ item, index }: ListRenderItemInfo<TWallet>) =>
item ? (
@ -379,7 +371,6 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
);
const flatListRef = useRef<FlatList<any>>(null);
useImperativeHandle(ref, (): any => {
return {
scrollToEnd: (params: { animated?: boolean | null | undefined } | undefined) => flatListRef.current?.scrollToEnd(params),
@ -401,10 +392,8 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
getNativeScrollRef: () => flatListRef.current?.getNativeScrollRef(),
};
}, []);
const onScrollToIndexFailed = (error: { averageItemLength: number; index: number }): void => {
console.debug('onScrollToIndexFailed');
console.debug(error);
console.debug('onScrollToIndexFailed', error);
flatListRef.current?.scrollToOffset({ offset: error.averageItemLength * error.index, animated: true });
setTimeout(() => {
if (data.length !== 0 && flatListRef.current !== null) {
@ -413,16 +402,16 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
}, 100);
};
const { width } = useWindowDimensions();
const sliderHeight = 195;
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
const keyExtractor = useCallback((item: TWallet, index: number) => (item?.getID ? item.getID() : index.toString()), []);
return isFlatList ? (
<FlatList
ref={flatListRef}
renderItem={renderItem}
extraData={data}
keyExtractor={(_, index) => index.toString()}
keyExtractor={keyExtractor}
showsVerticalScrollIndicator={false}
pagingEnabled={horizontal}
disableIntervalMomentum={horizontal}
@ -433,6 +422,7 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
showsHorizontalScrollIndicator={false}
initialNumToRender={10}
scrollEnabled={scrollEnabled}
keyboardShouldPersistTaps="handled"
ListHeaderComponent={ListHeaderComponent}
style={{ minHeight: sliderHeight + 12 }}
onScrollToIndexFailed={onScrollToIndexFailed}

View file

@ -40,9 +40,7 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
borderBottomColor: colors.lightBorder,
backgroundColor: colors.elevated,
},
list: {
color: colors.buttonTextColor,
},
index: {
color: colors.alternativeTextColor,
},
@ -151,24 +149,29 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
title={item.address}
actions={menuActions}
onPressMenuItem={onToolTipPress}
// Revisit once RNMenu has renderPreview prop
renderPreview={renderPreview}
onPress={navigateToReceive}
isButton
>
<ListItem key={item.key} containerStyle={stylesHook.container}>
<ListItem.Content style={stylesHook.list}>
<ListItem.Title style={stylesHook.list} numberOfLines={1} ellipsizeMode="middle">
<Text style={[styles.index, stylesHook.index]}>{item.index + 1}</Text>{' '}
<Text style={[stylesHook.address, styles.address]}>{item.address}</Text>
</ListItem.Title>
<View style={styles.subtitle}>
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>{balance}</Text>
<ListItem.Content>
<View style={styles.row}>
<View style={styles.leftSection}>
<Text style={[styles.index, stylesHook.index]}>{item.index}</Text>
</View>
<View style={styles.middleSection}>
<Text style={[stylesHook.address, styles.address]} numberOfLines={1} ellipsizeMode="middle">
{item.address}
</Text>
<Text style={[stylesHook.balance, styles.balance]}>{balance}</Text>
</View>
</View>
</ListItem.Content>
<View>
<View style={styles.rightContainer}>
<AddressTypeBadge isInternal={item.isInternal} hasTransactions={hasTransactions} />
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>
{loc.addresses.transactions}: {item.transactions}
<Text style={[stylesHook.balance, styles.balance]}>
{loc.addresses.transactions}: {item.transactions ?? 0}
</Text>
</View>
</ListItem>
@ -179,20 +182,27 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
const styles = StyleSheet.create({
address: {
fontWeight: 'bold',
marginHorizontal: 40,
marginHorizontal: 4,
},
index: {
fontSize: 15,
},
balance: {
marginTop: 8,
marginLeft: 14,
marginTop: 4,
},
subtitle: {
flex: 1,
row: {
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
alignItems: 'center',
},
leftSection: {
marginRight: 8,
},
middleSection: {
flex: 1,
},
rightContainer: {
justifyContent: 'center',
alignItems: 'flex-end',
},
});

View file

@ -1,6 +1,6 @@
import { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import React from 'react';
import { Image, Keyboard, StyleSheet, TouchableOpacity } from 'react-native';
import { Image, Keyboard, Platform, StyleSheet, TouchableOpacity } from 'react-native';
import loc from '../loc';
import { Theme } from './themes';
@ -59,7 +59,6 @@ const navigationStyle = (
{
closeButtonPosition,
onCloseButtonPressed,
headerBackVisible = true,
...opts
}: NativeStackNavigationOptions & {
closeButtonPosition?: CloseButtonPosition;
@ -78,11 +77,6 @@ const navigationStyle = (
let headerRight;
let headerLeft;
if (!headerBackVisible) {
headerLeft = () => <></>;
opts.headerLeft = headerLeft;
}
if (closeButton === CloseButtonPosition.Right) {
headerRight = () => (
<TouchableOpacity
@ -108,17 +102,24 @@ const navigationStyle = (
</TouchableOpacity>
);
}
let options: NativeStackNavigationOptions = {
const baseHeaderStyle = {
headerShadowVisible: false,
headerTitleStyle: {
fontWeight: '600',
fontWeight: '600' as const,
color: theme.colors.foregroundColor,
},
headerBackTitleVisible: false,
headerTintColor: theme.colors.foregroundColor,
headerBackButtonDisplayMode: 'minimal',
};
const isLeftCloseButtonAndroid = closeButton === CloseButtonPosition.Left && Platform.OS === 'android';
const leftCloseButtonStyle = isLeftCloseButtonAndroid ? { headerBackImageSource: theme.closeImage } : { headerLeft };
let options: NativeStackNavigationOptions = {
...baseHeaderStyle,
...leftCloseButtonStyle,
headerBackButtonDisplayMode: 'minimal',
headerRight,
headerLeft,
...opts,
};

View file

@ -128,18 +128,18 @@ end
You can test it on the following devices:
- [Google Pixel 9 (Android 15)](https://app-live.browserstack.com/dashboard#os=android&os_version=15.0&device=Google+Pixel+8&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
- [Google Pixel 8 (Android 14)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Google+Pixel+8&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
- [Google Pixel 7 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Google+Pixel+7&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
- [Google Pixel 5 (Android 12)](https://app-live.browserstack.com/dashboard#os=android&os_version=12.0&device=Google+Pixel+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
- [Google Pixel 3a (Android 9)](https://app-live.browserstack.com/dashboard#os=android&os_version=9.0&device=Google+Pixel+3a&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
- [Google Pixel 9 (Android 15)](https://app-live.browserstack.com/dashboard#os=android&os_version=15.0&device=Google+Pixel+8&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
- [Google Pixel 8 (Android 14)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Google+Pixel+8&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
- [Google Pixel 7 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Google+Pixel+7&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
- [Google Pixel 5 (Android 12)](https://app-live.browserstack.com/dashboard#os=android&os_version=12.0&device=Google+Pixel+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
- [Google Pixel 3a (Android 9)](https://app-live.browserstack.com/dashboard#os=android&os_version=9.0&device=Google+Pixel+3a&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
- [Samsung Galaxy Z Fold 6 (Android 14)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Samsung+Galaxy+Z+Fold+6&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
- [Samsung Galaxy Z Fold 5 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Z+Fold+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
- [Samsung Galaxy Tab S9 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Tab+S9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
- [Samsung Galaxy Note 9 (Android 8.1)](https://app-live.browserstack.com/dashboard#os=android&os_version=8.1&device=Samsung+Galaxy+Note+9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
- [Samsung Galaxy Z Fold 6 (Android 14)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Samsung+Galaxy+Z+Fold+6&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
- [Samsung Galaxy Z Fold 5 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Z+Fold+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
- [Samsung Galaxy Tab S9 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Tab+S9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
- [Samsung Galaxy Note 9 (Android 8.1)](https://app-live.browserstack.com/dashboard#os=android&os_version=8.1&device=Samsung+Galaxy+Note+9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
- [OnePlus 11R (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=OnePlus+11R&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
- [OnePlus 11R (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=OnePlus+11R&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
**Filename**: [#{apk_filename}](#{apk_download_url})
**BrowserStack App URL**: #{app_url}
COMMENT
@ -192,6 +192,40 @@ end
# ===========================
platform :ios do
# Add helper methods for error handling and retries
def ensure_env_vars(vars)
vars.each do |var|
UI.user_error!("#{var} environment variable is missing") if ENV[var].nil? || ENV[var].empty?
end
end
def log_success(message)
UI.success("✅ #{message}")
end
def log_error(message)
UI.error("❌ #{message}")
end
# Method to safely call actions with retry logic
def with_retry(max_attempts = 3, action_name = "")
attempts = 0
begin
attempts += 1
yield
rescue => e
if attempts < max_attempts
wait_time = 10 * attempts
log_error("Attempt #{attempts}/#{max_attempts} for #{action_name} failed: #{e.message}")
UI.message("Retrying in #{wait_time} seconds...")
sleep(wait_time)
retry
else
log_error("#{action_name} failed after #{max_attempts} attempts: #{e.message}")
raise e
end
end
end
desc "Register new devices from a file"
lane :register_devices_from_txt do
@ -238,26 +272,33 @@ platform :ios do
desc "Synchronize certificates and provisioning profiles"
lane :setup_provisioning_profiles do
required_vars = ["GIT_ACCESS_TOKEN", "GIT_URL", "ITC_TEAM_ID", "ITC_TEAM_NAME", "KEYCHAIN_PASSWORD"]
ensure_env_vars(required_vars)
UI.message("Setting up provisioning profiles...")
platform = "ios"
# Iterate over app identifiers to fetch provisioning profiles
app_identifiers.each do |app_identifier|
match(
git_basic_authorization: ENV["GIT_ACCESS_TOKEN"],
git_url: ENV["GIT_URL"],
type: "appstore",
clone_branch_directly: true, # Skip if the branch already exists
platform: platform,
app_identifier: app_identifier,
team_id: ENV["ITC_TEAM_ID"],
team_name: ENV["ITC_TEAM_NAME"],
readonly: true,
keychain_name: "temp_keychain",
keychain_password: ENV["KEYCHAIN_PASSWORD"]
)
with_retry(3, "Fetching provisioning profile for #{app_identifier}") do
UI.message("Fetching provisioning profile for #{app_identifier}...")
match(
git_basic_authorization: ENV["GIT_ACCESS_TOKEN"],
git_url: ENV["GIT_URL"],
type: "appstore",
clone_branch_directly: true,
platform: "ios",
app_identifier: app_identifier,
team_id: ENV["ITC_TEAM_ID"],
team_name: ENV["ITC_TEAM_NAME"],
readonly: true,
keychain_name: "temp_keychain",
keychain_password: ENV["KEYCHAIN_PASSWORD"]
)
log_success("Successfully fetched provisioning profile for #{app_identifier}")
end
end
log_success("All provisioning profiles set up")
end
desc "Fetch development certificates and provisioning profiles for Mac Catalyst"
@ -402,48 +443,114 @@ lane :upload_bugsnag_sourcemaps do
end
desc "Build the iOS app"
lane :build_app_lane do
Dir.chdir(project_root) do
UI.message("Building the application from: #{Dir.pwd}")
lane :build_app_lane do
Dir.chdir(project_root) do
UI.message("Building the application from: #{Dir.pwd}")
workspace_path = File.join(project_root, "ios", "BlueWallet.xcworkspace")
export_options_path = File.join(project_root, "ios", "export_options.plist")
workspace_path = File.join(project_root, "ios", "BlueWallet.xcworkspace")
export_options_path = File.join(project_root, "ios", "export_options.plist")
clear_derived_data_lane
begin
build_ios_app(
scheme: "BlueWallet",
workspace: workspace_path,
export_method: "app-store",
include_bitcode: false,
configuration: "Release",
skip_profile_detection: false,
include_symbols: true,
export_team_id: ENV["ITC_TEAM_ID"],
export_options: export_options_path,
output_directory: File.join(project_root, "ios", "build"),
output_name: "BlueWallet_#{ENV['PROJECT_VERSION']}_#{ENV['NEW_BUILD_NUMBER']}.ipa",
buildlog_path: File.join(project_root, "ios", "build_logs"),
silent: false,
clean: true
)
rescue => e
UI.user_error!("build_ios_app failed: #{e.message}")
end
clear_derived_data_lane
# Determine which iOS version to use
ios_version = determine_ios_version
# Use File.join to construct paths without extra slashes
ipa_path = lane_context[SharedValues::IPA_OUTPUT_PATH]
UI.message("Using iOS version: #{ios_version}")
UI.message("Using export options from: #{export_options_path}")
# Define the IPA output path before building
ipa_directory = File.join(project_root, "ios", "build")
ipa_name = "BlueWallet_#{ENV['PROJECT_VERSION']}_#{ENV['NEW_BUILD_NUMBER']}.ipa"
ipa_path = File.join(ipa_directory, ipa_name)
begin
build_ios_app(
scheme: "BlueWallet",
workspace: workspace_path,
export_method: "app-store",
export_options: export_options_path,
output_directory: ipa_directory,
output_name: ipa_name,
buildlog_path: File.join(project_root, "ios", "build_logs"),
)
rescue => e
UI.user_error!("build_ios_app failed: #{e.message}")
end
if ipa_path && File.exist?(ipa_path)
UI.message("IPA successfully found at: #{ipa_path}")
# Check for IPA path from both our defined path and fastlane's context
ipa_path = lane_context[SharedValues::IPA_OUTPUT_PATH] || ipa_path
# Ensure the directory exists
FileUtils.mkdir_p(File.dirname(ipa_path)) unless Dir.exist?(File.dirname(ipa_path))
if ipa_path && File.exist?(ipa_path)
UI.message("IPA successfully found at: #{ipa_path}")
else
# Try to find any IPA file as fallback
Dir.chdir(project_root) do
fallback_ipa = Dir.glob("**/*.ipa").first
if fallback_ipa
ipa_path = File.join(project_root, fallback_ipa)
UI.message("Found fallback IPA at: #{ipa_path}")
else
UI.user_error!("No IPA file found after build")
end
end
end
# Set both environment variable and GitHub Actions output
ENV['IPA_OUTPUT_PATH'] = ipa_path
sh("echo 'IPA_OUTPUT_PATH=#{ipa_path}' >> $GITHUB_ENV") # Export for GitHub Actions
else
UI.user_error!("IPA not found after build_ios_app.")
# Set both standard output format and the newer GITHUB_OUTPUT format
sh("echo 'ipa_output_path=#{ipa_path}' >> $GITHUB_OUTPUT") if ENV['GITHUB_OUTPUT']
sh("echo ::set-output name=ipa_output_path::#{ipa_path}")
# Also write path to a file that can be read by subsequent steps
ipa_path_file = "#{ipa_directory}/ipa_path.txt"
File.write(ipa_path_file, ipa_path)
UI.success("Saved IPA path to: #{ipa_path_file}")
end
end
end
desc "Delete temporary keychain"
lane :delete_temp_keychain do
UI.message("Deleting temporary keychain...")
delete_keychain(
name: "temp_keychain"
) if File.exist?(File.expand_path("~/Library/Keychains/temp_keychain-db"))
UI.message("Temporary keychain deleted successfully.")
end
# Helper method to determine which iOS version to use
private_lane :determine_ios_version do
# Get available iOS simulator runtimes
runtimes_output = sh("xcrun simctl list runtimes 2>&1", log: false) rescue ""
if runtimes_output.include?("iOS")
# Extract available iOS versions
ios_versions = runtimes_output.scan(/iOS ([0-9.]+)/)
.flatten
.map { |v| Gem::Version.new(v) }
.sort
.reverse
if ios_versions.any?
latest_version = ios_versions.first.to_s
UI.success("Found iOS simulator version: #{latest_version}")
latest_version # Implicit return - last expression is returned
else
# Default to a reasonable iOS version if none found
UI.important("No iOS simulator versions found. Using default version.")
"17.6" # Implicit return
end
else
# Default to a reasonable iOS version if no iOS runtimes
UI.important("No iOS simulator runtimes found. Using default version.")
"17.6" # Implicit return
end
end
end
# ===========================
# Global Lanes

View file

@ -3,33 +3,39 @@
# URL of the Git repository to store the certificates
git_url(ENV["GIT_URL"])
# Define the type of match to run, could be one of 'appstore', 'adhoc', 'development', or 'enterprise'.
# For example, use 'appstore' for App Store builds, 'adhoc' for Ad Hoc distribution,
# 'development' for development builds, and 'enterprise' for In-House (enterprise) distribution.
type("appstore")
# Define the type of match to run
# Default to "appstore" but can be overridden
type(ENV["MATCH_TYPE"] || "appstore")
app_identifier(["io.bluewallet.bluewallet", "io.bluewallet.bluewallet.watch", "io.bluewallet.bluewallet.watch.extension", "io.bluewallet.bluewallet.Stickers", "io.bluewallet.bluewallet.MarketWidget"]) # Replace with your app identifiers
# App identifiers for all BlueWallet apps
app_identifier([
"io.bluewallet.bluewallet",
"io.bluewallet.bluewallet.watch",
"io.bluewallet.bluewallet.watch.extension",
"io.bluewallet.bluewallet.Stickers",
"io.bluewallet.bluewallet.MarketWidget"
])
# List of app identifiers to create provisioning profiles for.
# Replace with your app's bundle identifier(s).
# Your Apple Developer account email address.
# Your Apple Developer account email address
username(ENV["APPLE_ID"])
# The ID of your Apple Developer team if you're part of multiple teams
# The ID of your Apple Developer team
team_id(ENV["ITC_TEAM_ID"])
# Set this to true if match should only read existing certificates and profiles
# and not create new ones.
readonly(true)
# Set readonly based on environment (default to true for safety)
# Set to false explicitly when new profiles need to be created
readonly(ENV["MATCH_READONLY"] == "false" ? false : true)
# Optional: The Git branch that is used for match.
# Default is 'master'.
# Optional: Path to a specific SSH key to be used by match.
# Only needed if you're using a private repository and match needs to use SSH keys for authentication.
# ssh_key("/path/to/your/private/key")
# Optional: Define the platform to use, can be 'ios', 'macos', or 'tvos'.
# For React Native projects, you'll typically use 'ios'.
# Define the platform to use
platform("ios")
# Git basic authentication through access token
# This is useful for CI/CD environments where SSH keys aren't available
git_basic_authorization(ENV["GIT_ACCESS_TOKEN"])
# Storage mode (git by default)
storage_mode("git")
# Optional: The Git branch that is used for match
# Default is 'master'
# branch("main")

View file

@ -4,3 +4,4 @@
gem 'fastlane-plugin-browserstack'
gem 'fastlane-plugin-bugsnag_sourcemaps_upload'
gem "fastlane-plugin-bugsnag"

1
gesture-handler.js Normal file
View file

@ -0,0 +1 @@
// Don't import react-native-gesture-handler on web

View file

@ -0,0 +1,2 @@
// Only import react-native-gesture-handler on native platforms
import 'react-native-gesture-handler';

View file

@ -1,5 +1,6 @@
import { Platform } from 'react-native';
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { navigationRef } from '../NavigationService.ts';
const isCameraAuthorizationStatusGranted = async () => {
const status = await check(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA);
@ -10,4 +11,17 @@ const requestCameraAuthorization = () => {
return request(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA);
};
export { isCameraAuthorizationStatusGranted, requestCameraAuthorization };
const scanQrHelper = async (): Promise<string> => {
await requestCameraAuthorization();
return new Promise(resolve => {
if (navigationRef.isReady()) {
navigationRef.current?.navigate('ScanQRCode', {
onBarScanned: (data: string) => {
resolve(data);
},
});
}
});
};
export { isCameraAuthorizationStatusGranted, requestCameraAuthorization, scanQrHelper };

23
helpers/screenProtect.ts Normal file
View file

@ -0,0 +1,23 @@
// import { enableSecureView, disableSecureView, forbidAndroidShare, allowAndroidShare } from 'react-native-prevent-screenshot-ios-android';
// import { Platform } from 'react-native';
// import { isDesktop } from '../blue_modules/environment';
export const enableScreenProtect = () => {
// if (isDesktop) return;
// if (Platform.OS === 'ios') {
// enableSecureView();
// } else if (Platform.OS === 'android') {
// forbidAndroidShare();
// }
};
export const disableScreenProtect = () => {
// if (isDesktop) return;
// if (Platform.OS === 'ios') {
// disableSecureView();
// } else if (Platform.OS === 'android') {
// allowAndroidShare();
// }
};
// CURRENTLY UNUSED AS WE WAIT FOR NAV 7 SUPPORT

View file

@ -1,5 +1,3 @@
import 'react-native-gesture-handler'; // should be on top
import { CommonActions } from '@react-navigation/native';
import { useCallback, useEffect, useRef } from 'react';
import { AppState, AppStateStatus, Linking } from 'react-native';
@ -21,25 +19,42 @@ import loc from '../loc';
import { Chain } from '../models/bitcoinUnits';
import { navigationRef } from '../NavigationService';
import ActionSheet from '../screen/ActionSheet';
import { useStorage } from '../hooks/context/useStorage';
import { useStorage } from './context/useStorage';
import RNQRGenerator from 'rn-qr-generator';
import presentAlert from './Alert';
import useMenuElements from '../hooks/useMenuElements';
import useWidgetCommunication from '../hooks/useWidgetCommunication';
import useWatchConnectivity from '../hooks/useWatchConnectivity';
import useDeviceQuickActions from '../hooks/useDeviceQuickActions';
import useHandoffListener from '../hooks/useHandoffListener';
import presentAlert from '../components/Alert';
import useWidgetCommunication from './useWidgetCommunication';
import useWatchConnectivity from './useWatchConnectivity';
import useDeviceQuickActions from './useDeviceQuickActions';
import useHandoffListener from './useHandoffListener';
import useMenuElements from './useMenuElements';
const ClipboardContentType = Object.freeze({
BITCOIN: 'BITCOIN',
LIGHTNING: 'LIGHTNING',
});
const CompanionDelegates = () => {
const { wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions, setSharedCosigner } = useStorage();
/**
* Hook that initializes all companion listeners and functionality without rendering a component
*/
const useCompanionListeners = (skipIfNotInitialized = true) => {
const {
wallets,
addWallet,
saveToDisk,
fetchAndSaveWalletTransactions,
refreshAllWalletTransactions,
setSharedCosigner,
walletsInitialized,
} = useStorage();
const appState = useRef<AppStateStatus>(AppState.currentState);
const clipboardContent = useRef<undefined | string>();
// We need to call hooks unconditionally before any conditional logic
// We'll use this check inside the effects to conditionally run logic
const shouldActivateListeners = !skipIfNotInitialized || walletsInitialized;
// Initialize other hooks regardless of activation status
// They'll handle their own conditional logic internally
useWatchConnectivity();
useWidgetCommunication();
useMenuElements();
@ -47,6 +62,8 @@ const CompanionDelegates = () => {
useHandoffListener();
const processPushNotifications = useCallback(async () => {
if (!shouldActivateListeners) return false;
await new Promise(resolve => setTimeout(resolve, 200));
try {
const notifications2process = await getStoredNotifications();
@ -166,49 +183,58 @@ const CompanionDelegates = () => {
console.error('Failed to process push notifications:', error);
}
return false;
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets]);
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets, shouldActivateListeners]);
useEffect(() => {
if (!shouldActivateListeners) return;
initializeNotifications(processPushNotifications);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [shouldActivateListeners]);
const handleOpenURL = useCallback(
async (event: { url: string }): Promise<void> => {
const { url } = event;
if (!shouldActivateListeners) return;
if (url) {
const decodedUrl = decodeURIComponent(url);
const fileName = decodedUrl.split('/').pop()?.toLowerCase();
if (fileName && /\.(jpe?g|png)$/i.test(fileName)) {
try {
if (!event.url) return;
let decodedUrl: string;
try {
decodedUrl = decodeURIComponent(event.url);
} catch (e) {
console.error('Failed to decode URL, using original', e);
decodedUrl = event.url;
}
const fileName = decodedUrl.split('/').pop()?.toLowerCase() || '';
if (/\.(jpe?g|png)$/i.test(fileName)) {
let qrResult;
try {
if (!decodedUrl) {
throw new Error(loc.send.qr_error_no_qrcode);
qrResult = await RNQRGenerator.detect({ uri: decodedUrl });
} catch (e) {
console.error('QR detection first attempt failed:', e);
}
if (!qrResult || !qrResult.values || qrResult.values.length === 0) {
const altUrl = decodedUrl.replace(/^file:\/\//, '');
try {
qrResult = await RNQRGenerator.detect({ uri: altUrl });
} catch (e) {
console.error('QR detection second attempt failed:', e);
}
const values = await RNQRGenerator.detect({
uri: decodedUrl,
});
if (values && values.values.length > 0) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
DeeplinkSchemaMatch.navigationRouteFor(
{ url: values.values[0] },
(value: [string, any]) => navigationRef.navigate(...value),
{
wallets,
addWallet,
saveToDisk,
setSharedCosigner,
},
);
} else {
throw new Error(loc.send.qr_error_no_qrcode);
}
} catch (error) {
console.error('Error detecting QR code:', error);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.send.qr_error_no_qrcode });
}
if (qrResult?.values?.length) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
DeeplinkSchemaMatch.navigationRouteFor(
{ url: qrResult.values[0] },
(value: [string, any]) => navigationRef.navigate(...value),
{
wallets,
addWallet,
saveToDisk,
setSharedCosigner,
},
);
} else {
throw new Error(loc.send.qr_error_no_qrcode);
}
} else {
DeeplinkSchemaMatch.navigationRouteFor(event, (value: [string, any]) => navigationRef.navigate(...value), {
@ -218,12 +244,19 @@ const CompanionDelegates = () => {
setSharedCosigner,
});
}
} catch (err: any) {
console.error('Error in handleOpenURL:', err);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: err.message || loc.send.qr_error_no_qrcode });
}
},
[wallets, addWallet, saveToDisk, setSharedCosigner],
[wallets, addWallet, saveToDisk, setSharedCosigner, shouldActivateListeners],
);
const showClipboardAlert = useCallback(
({ contentType }: { contentType: undefined | string }) => {
if (!shouldActivateListeners) return;
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
getClipboardContent().then(clipboard => {
if (!clipboard) return;
@ -246,12 +279,13 @@ const CompanionDelegates = () => {
);
});
},
[handleOpenURL],
[handleOpenURL, shouldActivateListeners],
);
const handleAppStateChange = useCallback(
async (nextAppState: AppStateStatus | undefined) => {
if (wallets.length === 0) return;
if (!shouldActivateListeners || wallets.length === 0) return;
if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) {
setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000);
updateExchangeRate();
@ -291,10 +325,12 @@ const CompanionDelegates = () => {
appState.current = nextAppState;
}
},
[processPushNotifications, showClipboardAlert, wallets],
[processPushNotifications, showClipboardAlert, wallets, shouldActivateListeners],
);
const addListeners = useCallback(() => {
if (!shouldActivateListeners) return { urlSubscription: null, appStateSubscription: null };
const urlSubscription = Linking.addEventListener('url', handleOpenURL);
const appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
@ -302,18 +338,16 @@ const CompanionDelegates = () => {
urlSubscription,
appStateSubscription,
};
}, [handleOpenURL, handleAppStateChange]);
}, [handleOpenURL, handleAppStateChange, shouldActivateListeners]);
useEffect(() => {
const subscriptions = addListeners();
return () => {
subscriptions.urlSubscription?.remove();
subscriptions.appStateSubscription?.remove();
subscriptions.urlSubscription?.remove?.();
subscriptions.appStateSubscription?.remove?.();
};
}, [addListeners]);
return null;
};
export default CompanionDelegates;
export default useCompanionListeners;

View file

@ -1,23 +1,27 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo } from 'react';
import debounce from '../blue_modules/debounce';
const useDebounce = <T>(value: T, delay: number): T => {
// Overload signatures
function useDebounce<T extends (...args: any[]) => any>(callback: T, delay: number): T;
function useDebounce<T>(value: T, delay: number): T;
function useDebounce<T>(value: T, delay: number): T {
const isFn = typeof value === 'function';
const debouncedFunction = useMemo(() => {
return isFn ? debounce(value as unknown as (...args: any[]) => any, delay) : null;
}, [isFn, value, delay]);
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = debounce((val: T) => {
setDebouncedValue(val);
}, delay);
if (!isFn) {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}
}, [isFn, value, delay]);
handler(value);
return () => {
handler.cancel();
};
}, [value, delay]);
return debouncedValue;
};
return isFn ? (debouncedFunction as unknown as T) : debouncedValue;
}
export default useDebounce;

View file

@ -7,7 +7,12 @@ import { requestCameraAuthorization } from '../helpers/scan-qr';
import { useCallback, useMemo } from 'react';
// List of screens that require biometrics
const requiresBiometrics = ['WalletExportRoot', 'WalletXpubRoot', 'ViewEditMultisigCosignersRoot', 'ExportMultisigCoordinationSetupRoot'];
const requiresBiometrics = [
'WalletExportRoot',
'WalletXpubRoot',
'ViewEditMultisigCosigners',
'ExportMultisigCoordinationSetupRoot',
];
// List of screens that require wallet export to be saved
const requiresWalletExportIsSaved = ['ReceiveDetailsRoot', 'WalletAddresses'];
@ -17,8 +22,25 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
const { wallets, saveToDisk } = useStorage();
const { isBiometricUseEnabled } = useBiometrics();
const enhancedNavigate: NavigationProp<ParamListBase>['navigate'] = useCallback(
(screenOrOptions: any, params?: any, options?: { merge?: boolean }) => {
const enhancedNavigate = useCallback(
(
...args:
| [string]
| [string, object | undefined]
| [string, object | undefined, { merge?: boolean }]
| [{ name: string; params?: object; path?: string; merge?: boolean }]
) => {
let screenOrOptions: any;
let params: any;
let options: { merge?: boolean } | undefined;
if (typeof args[0] === 'string') {
screenOrOptions = args[0];
params = args[1];
options = args[2];
} else {
screenOrOptions = args[0];
}
let screenName: string;
if (typeof screenOrOptions === 'string') {
screenName = screenOrOptions;
@ -44,6 +66,13 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
};
(async () => {
// NEW: If the current (active) screen is 'ScanQRCode', bypass all checks.
const currentRouteName = navigationRef.current?.getCurrentRoute()?.name;
if (currentRouteName === 'ScanQRCode') {
proceedWithNavigation();
return;
}
if (isRequiresBiometrics) {
const isBiometricsEnabled = await isBiometricUseEnabled();
if (isBiometricsEnabled) {
@ -53,8 +82,8 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
return;
} else {
console.error('Biometric authentication failed');
// Decide if navigation should proceed or not after failed authentication
return; // Prevent proceeding with the original navigation if bio fails
// Do not proceed if authentication fails.
return;
}
}
}
@ -78,18 +107,17 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
await saveToDisk();
proceedWithNavigation();
} catch (error) {
if (error) {
originalNavigation.navigate('WalletExportRoot', {
screen: 'WalletExport',
params: { walletID },
});
}
// If there was an error (or the user cancelled), navigate to the wallet export screen.
originalNavigation.navigate('WalletExportRoot', {
screen: 'WalletExport',
params: { walletID },
});
}
return; // Prevent proceeding with the original navigation if the reminder is shown
return; // Do not proceed with the original navigation if reminder was shown.
}
}
// If the target screen is ScanQRCode, request camera authorization.
if (screenName === 'ScanQRCode') {
await requestCameraAuthorization();
}
@ -115,4 +143,4 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
// Usage example:
// type NavigationProps = NativeStackNavigationProp<SendDetailsStackParamList, 'SendDetails'>;
// const navigation = useExtendedNavigation<NavigationProps>();
// const navigation = useExtendedNavigation<NavigationProps>();

View file

@ -23,20 +23,25 @@ const useHandoffListener = () => {
const handleUserActivity = useCallback(
(data: UserActivityData) => {
if (!data || !data.activityType) {
console.debug(`Invalid handoff data received: ${data ? JSON.stringify(data) : 'No data provided'}`);
return;
}
const { activityType, userInfo } = data;
const modifiedUserInfo = { ...(userInfo || {}), type: activityType };
try {
if (activityType === HandOffActivityType.ReceiveOnchain) {
if (activityType === HandOffActivityType.ReceiveOnchain && modifiedUserInfo.address) {
navigate('ReceiveDetailsRoot', {
screen: 'ReceiveDetails',
params: { address: userInfo.address },
params: { address: modifiedUserInfo.address, type: activityType },
});
} else if (activityType === HandOffActivityType.Xpub) {
} else if (activityType === HandOffActivityType.Xpub && modifiedUserInfo.xpub) {
navigate('WalletXpubRoot', {
screen: 'WalletXpub',
params: { xpub: userInfo.xpub },
params: { xpub: modifiedUserInfo.xpub, type: activityType },
});
} else {
console.debug(`Unhandled activity type: ${activityType}`);
console.debug(`Unhandled or incomplete activity type/data: ${activityType}`, modifiedUserInfo);
}
} catch (error) {
console.error('Error handling user activity:', error);
@ -50,9 +55,13 @@ const useHandoffListener = () => {
const activitySubscription = eventEmitter?.addListener('onUserActivityOpen', handleUserActivity);
EventEmitter.getMostRecentUserActivity?.()
.then(handleUserActivity)
.catch(() => console.debug('No userActivity object sent'));
if (EventEmitter && EventEmitter.getMostRecentUserActivity) {
EventEmitter.getMostRecentUserActivity()
.then(handleUserActivity)
.catch(() => console.debug('No valid user activity object received'));
} else {
console.debug('EventEmitter native module is not available.');
}
return () => {
activitySubscription?.remove();

View file

@ -1,68 +1,168 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useEffect, useCallback } from 'react';
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
import { navigationRef } from '../NavigationService';
import { CommonActions } from '@react-navigation/native';
import * as NavigationService from '../NavigationService';
import { useStorage } from './context/useStorage';
/*
Hook for managing iPadOS and macOS menu actions with keyboard shortcuts.
Uses MenuElementsEmitter for event handling.
Uses MenuElementsEmitter for event handling and navigation state.
*/
type MenuActionHandler = () => void;
// Singleton setup - initialize once at module level
const { MenuElementsEmitter } = NativeModules;
const eventEmitter =
(Platform.OS === 'ios' || Platform.OS === 'macos') && MenuElementsEmitter ? new NativeEventEmitter(MenuElementsEmitter) : null;
let eventEmitter: NativeEventEmitter | null = null;
let listenersInitialized = false;
const useMenuElements = () => {
const { walletsInitialized } = useStorage();
const reloadTransactionsMenuActionRef = useRef<() => void>(() => {});
// Registry for transaction handlers by screen ID
const handlerRegistry = new Map<string, MenuActionHandler>();
const setReloadTransactionsMenuActionFunction = useCallback((newFunction: () => void) => {
console.debug('Setting reloadTransactionsMenuActionFunction.');
reloadTransactionsMenuActionRef.current = newFunction;
}, []);
// Store subscription references for proper cleanup
let subscriptions: { remove: () => void }[] = [];
const dispatchNavigate = useCallback((routeName: string, screen?: string) => {
NavigationService.dispatch(CommonActions.navigate({ name: routeName, params: screen ? { screen } : undefined }));
}, []);
// Create a more robust emitter with error handling
try {
if (Platform.OS === 'ios' && MenuElementsEmitter) {
eventEmitter = new NativeEventEmitter(MenuElementsEmitter);
}
} catch (error) {
console.warn('[MenuElements] Failed to initialize event emitter: ', error);
eventEmitter = null;
}
const eventActions = useMemo(
() => ({
openSettings: () => dispatchNavigate('Settings'),
addWallet: () => dispatchNavigate('AddWalletRoot'),
importWallet: () => dispatchNavigate('AddWalletRoot', 'ImportWallet'),
reloadTransactions: () => {
console.debug('Calling reloadTransactionsMenuActionFunction');
reloadTransactionsMenuActionRef.current?.();
},
}),
[dispatchNavigate],
);
/**
* Safely navigate using multiple fallback approaches
*/
function safeNavigate(routeName: string, params?: Record<string, any>): void {
try {
if (navigationRef.current?.isReady()) {
navigationRef.current.navigate(routeName as never, params as never);
return;
}
if (navigationRef.isReady()) {
navigationRef.dispatch(
CommonActions.navigate({
name: routeName,
params,
}),
);
}
} catch (error) {
console.error(`[MenuElements] Navigation error:`, error);
}
}
// Cleanup event listeners to prevent memory leaks
function cleanupListeners(): void {
if (subscriptions.length > 0) {
subscriptions.forEach(subscription => {
try {
subscription.remove();
} catch (e) {
console.warn('[MenuElements] Error removing subscription:', e);
}
});
subscriptions = [];
listenersInitialized = false;
}
}
function initializeListeners(): void {
if (!eventEmitter || listenersInitialized) return;
cleanupListeners();
// Navigation actions
const globalActions = {
navigateToSettings: (): void => {
safeNavigate('Settings');
},
navigateToAddWallet: (): void => {
safeNavigate('AddWalletRoot');
},
navigateToImportWallet: (): void => {
safeNavigate('AddWalletRoot', { screen: 'ImportWallet' });
},
executeReloadTransactions: (): void => {
const currentRoute = navigationRef.current?.getCurrentRoute();
if (!currentRoute) return;
const screenName = currentRoute.name;
const params = (currentRoute.params as { walletID?: string }) || {};
const walletID = params.walletID;
const specificKey = walletID ? `${screenName}-${walletID}` : null;
const specificHandler = specificKey ? handlerRegistry.get(specificKey) : undefined;
const genericHandler = handlerRegistry.get(screenName);
const handler = specificHandler || genericHandler;
if (typeof handler === 'function') {
handler();
}
},
};
try {
subscriptions.push(eventEmitter.addListener('openSettings', globalActions.navigateToSettings));
subscriptions.push(eventEmitter.addListener('addWalletMenuAction', globalActions.navigateToAddWallet));
subscriptions.push(eventEmitter.addListener('importWalletMenuAction', globalActions.navigateToImportWallet));
subscriptions.push(eventEmitter.addListener('reloadTransactionsMenuAction', globalActions.executeReloadTransactions));
} catch (error) {
console.error('[MenuElements] Error setting up event listeners:', error);
}
listenersInitialized = true;
}
interface MenuElementsHook {
registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean;
unregisterTransactionsHandler: (screenKey: string) => void;
isMenuElementsSupported: boolean;
}
const mountedComponents = new Set<string>();
const useMenuElements = (): MenuElementsHook => {
useEffect(() => {
if (!walletsInitialized || !eventEmitter) return;
initializeListeners();
console.debug('Setting up menu event listeners');
// Add permanent listeners only once
eventEmitter.removeAllListeners('openSettings');
eventEmitter.removeAllListeners('addWalletMenuAction');
eventEmitter.removeAllListeners('importWalletMenuAction');
eventEmitter.addListener('openSettings', eventActions.openSettings);
eventEmitter.addListener('addWalletMenuAction', eventActions.addWallet);
eventEmitter.addListener('importWalletMenuAction', eventActions.importWallet);
const reloadTransactionsListener = eventEmitter.addListener('reloadTransactionsMenuAction', eventActions.reloadTransactions);
const unsubscribe = navigationRef.addListener('state', () => {});
return () => {
console.debug('Removing reloadTransactionsMenuAction listener');
reloadTransactionsListener.remove();
unsubscribe();
};
}, [walletsInitialized, eventActions]);
}, []);
const registerTransactionsHandler = useCallback((handler: MenuActionHandler, screenKey?: string): boolean => {
if (typeof handler !== 'function') return false;
const key = screenKey || navigationRef.current?.getCurrentRoute()?.name;
if (!key) return false;
mountedComponents.add(key);
handlerRegistry.set(key, handler);
return true;
}, []);
const unregisterTransactionsHandler = useCallback((screenKey: string): void => {
if (!screenKey) return;
handlerRegistry.delete(screenKey);
mountedComponents.delete(screenKey);
}, []);
return {
setReloadTransactionsMenuActionFunction,
registerTransactionsHandler,
unregisterTransactionsHandler,
isMenuElementsSupported: !!eventEmitter,
};
};

View file

@ -1,8 +1,28 @@
const useMenuElements = () => {
const setReloadTransactionsMenuActionFunction = (_: () => void) => {};
import { useCallback } from 'react';
type MenuActionHandler = () => void;
interface MenuElementsHook {
registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean;
unregisterTransactionsHandler: (screenKey: string) => void;
isMenuElementsSupported: boolean;
}
// Default implementation for platforms other than iOS
const useMenuElements = (): MenuElementsHook => {
const registerTransactionsHandler = useCallback((_handler: MenuActionHandler, _screenKey?: string): boolean => {
// Non-functional stub for non-iOS platforms
return false;
}, []);
const unregisterTransactionsHandler = useCallback((_screenKey: string): void => {
// No-op for non-supported platforms
}, []);
return {
setReloadTransactionsMenuActionFunction,
registerTransactionsHandler,
unregisterTransactionsHandler,
isMenuElementsSupported: false, // Not supported on platforms other than iOS
};
};

View file

@ -1,3 +1,4 @@
import './gesture-handler';
import './shim.js';
import React, { useEffect } from 'react';
@ -12,7 +13,12 @@ if (!Error.captureStackTrace) {
Error.captureStackTrace = () => {};
}
LogBox.ignoreLogs(['Require cycle:', 'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.']);
LogBox.ignoreLogs([
'Require cycle:',
'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.',
'Open debugger to view warnings.',
'Non-serializable values were found in the navigation state',
]);
const BlueAppComponent = () => {
useEffect(() => {

View file

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objectVersion = 63;
objects = {
/* Begin PBXBuildFile section */
@ -14,7 +14,6 @@
32F0A29A2311DBB20095C559 /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F0A2992311DBB20095C559 /* ComplicationController.swift */; };
6D2A6464258BA92D0092292B /* Stickers.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6D2A6463258BA92D0092292B /* Stickers.xcassets */; };
6D2A6468258BA92D0092292B /* Stickers.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6D2A6461258BA92C0092292B /* Stickers.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
6D32C5C62596CE3A008C077C /* EventEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D32C5C52596CE3A008C077C /* EventEmitter.m */; };
6D4AF15925D21172009DD853 /* MarketAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D9A2E6A254BAB1B007B5B82 /* MarketAPI.swift */; };
6D4AF16D25D21192009DD853 /* Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEB4BFA254FBA0E00E9F9AA /* Placeholders.swift */; };
6D4AF17825D211A3009DD853 /* FiatUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D2AA8072568B8F40090B089 /* FiatUnit.swift */; };
@ -46,6 +45,8 @@
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B9D9B3A7B2CB4255876B67AF /* libz.tbd */; };
849047CA2702A32A008EE567 /* Handoff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849047C92702A32A008EE567 /* Handoff.swift */; };
84E05A842721191B001A0D3A /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 84E05A832721191B001A0D3A /* Settings.bundle */; };
B409AB042D71DFAA00BA06F8 /* MenuElementsEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = B409AB032D71DFAA00BA06F8 /* MenuElementsEmitter.m */; };
B409AB062D71E07500BA06F8 /* MenuElementsEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B409AB052D71E07500BA06F8 /* MenuElementsEmitter.swift */; };
B40D4E34225841EC00428FCC /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E32225841EC00428FCC /* Interface.storyboard */; };
B40D4E36225841ED00428FCC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; };
B40D4E3D225841ED00428FCC /* BlueWalletWatch Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = B40D4E3C225841ED00428FCC /* BlueWalletWatch Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@ -113,12 +114,11 @@
B440340F2BCC40A400162242 /* fiatUnits.json in Resources */ = {isa = PBXBuildFile; fileRef = B440340E2BCC40A400162242 /* fiatUnits.json */; };
B44034102BCC40A400162242 /* fiatUnits.json in Resources */ = {isa = PBXBuildFile; fileRef = B440340E2BCC40A400162242 /* fiatUnits.json */; };
B44034112BCC40A400162242 /* fiatUnits.json in Resources */ = {isa = PBXBuildFile; fileRef = B440340E2BCC40A400162242 /* fiatUnits.json */; };
B44305BC2D6A04B2004675CC /* CustomSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = B44305BB2D6A04B2004675CC /* CustomSegmentedControl.m */; };
B450109C2C0FCD8A00619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
B450109D2C0FCD9F00619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
B450109F2C0FCDA500619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */; };
B4549F362B82B10D002E3153 /* ci_post_clone.sh in Resources */ = {isa = PBXBuildFile; fileRef = B4549F352B82B10D002E3153 /* ci_post_clone.sh */; };
B45942C42CDECF2400B3DC2E /* MenuElementsEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = B4C075292CDDB3C500322A84 /* MenuElementsEmitter.m */; };
B461B852299599F800E431AA /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = B461B851299599F800E431AA /* AppDelegate.mm */; };
B4742E972CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
B4742E982CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
@ -156,6 +156,8 @@
B4AB225E2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; };
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; };
B4B1A4642BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; };
B4B3EC222D69FF6C00327F3D /* CustomSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */; };
B4B3EC252D69FF8700327F3D /* EventEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EC232D69FF8700327F3D /* EventEmitter.swift */; };
B4D0B2622C1DEA11006B6B1B /* ReceivePageInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2612C1DEA11006B6B1B /* ReceivePageInterfaceController.swift */; };
B4D0B2642C1DEA99006B6B1B /* ReceiveType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2632C1DEA99006B6B1B /* ReceiveType.swift */; };
B4D0B2662C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */; };
@ -294,8 +296,6 @@
6D2A6463258BA92D0092292B /* Stickers.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Stickers.xcassets; sourceTree = "<group>"; };
6D2A6465258BA92D0092292B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
6D2AA8072568B8F40090B089 /* FiatUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiatUnit.swift; sourceTree = "<group>"; };
6D32C5C42596CE2F008C077C /* EventEmitter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EventEmitter.h; sourceTree = "<group>"; };
6D32C5C52596CE3A008C077C /* EventEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EventEmitter.m; sourceTree = "<group>"; };
6D333B3A252FE1A3004D72DF /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
6D333B3C252FE1A3004D72DF /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
6D4AF18225D215D0009DD853 /* BlueWalletWatch-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BlueWalletWatch-Bridging-Header.h"; sourceTree = "<group>"; };
@ -336,6 +336,8 @@
9F1F51A83D044F3BB26A35FC /* libRNSVG-tvOS.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = "libRNSVG-tvOS.a"; sourceTree = "<group>"; };
A7C4B1FDAD264618BAF8C335 /* libRNCWebView.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNCWebView.a; sourceTree = "<group>"; };
AB2325650CE04F018697ACFE /* libRNReactNativeHapticFeedback.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNReactNativeHapticFeedback.a; sourceTree = "<group>"; };
B409AB032D71DFAA00BA06F8 /* MenuElementsEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = MenuElementsEmitter.m; path = MenuElementsEmitter/MenuElementsEmitter.m; sourceTree = SOURCE_ROOT; };
B409AB052D71E07500BA06F8 /* MenuElementsEmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MenuElementsEmitter.swift; path = MenuElementsEmitter/MenuElementsEmitter.swift; sourceTree = SOURCE_ROOT; };
B40D4E30225841EC00428FCC /* BlueWalletWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BlueWalletWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
B40D4E33225841EC00428FCC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = "<group>"; };
B40D4E35225841ED00428FCC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -372,9 +374,8 @@
B44033F82BCC379200162242 /* WidgetDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDataStore.swift; sourceTree = "<group>"; };
B44033FF2BCC37F800162242 /* Bundle+decode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+decode.swift"; sourceTree = "<group>"; };
B440340E2BCC40A400162242 /* fiatUnits.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = fiatUnits.json; path = ../../../models/fiatUnits.json; sourceTree = "<group>"; };
B44305BB2D6A04B2004675CC /* CustomSegmentedControl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomSegmentedControl.m; sourceTree = "<group>"; };
B450109B2C0FCD8A00619044 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomSegmentedControlManager.m; sourceTree = "<group>"; };
B45010A92C15080500619044 /* CustomSegmentedControlManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomSegmentedControlManager.h; sourceTree = "<group>"; };
B4549F352B82B10D002E3153 /* ci_post_clone.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = ci_post_clone.sh; sourceTree = "<group>"; };
B461B850299599F800E431AA /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = BlueWallet/AppDelegate.h; sourceTree = "<group>"; };
B461B851299599F800E431AA /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = BlueWallet/AppDelegate.mm; sourceTree = "<group>"; };
@ -396,8 +397,8 @@
B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLParserDelegate.swift; sourceTree = "<group>"; };
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetHelper.swift; sourceTree = "<group>"; };
B4B31A352C77BBA000663334 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Interface.strings; sourceTree = "<group>"; };
B4C075282CDDB3BE00322A84 /* MenuElementsEmitter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MenuElementsEmitter.h; sourceTree = "<group>"; };
B4C075292CDDB3C500322A84 /* MenuElementsEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MenuElementsEmitter.m; sourceTree = "<group>"; };
B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSegmentedControl.swift; sourceTree = "<group>"; };
B4B3EC232D69FF8700327F3D /* EventEmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventEmitter.swift; sourceTree = "<group>"; };
B4D0B2612C1DEA11006B6B1B /* ReceivePageInterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceivePageInterfaceController.swift; sourceTree = "<group>"; };
B4D0B2632C1DEA99006B6B1B /* ReceiveType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveType.swift; sourceTree = "<group>"; };
B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveInterfaceMode.swift; sourceTree = "<group>"; };
@ -489,7 +490,6 @@
13B07FAE1A68108700A75B9A /* BlueWallet */ = {
isa = PBXGroup;
children = (
B4C0752B2CDDB3CC00322A84 /* MenuElementsEmitter */,
B461B850299599F800E431AA /* AppDelegate.h */,
B461B851299599F800E431AA /* AppDelegate.mm */,
32C7944323B8879D00BE2AFA /* BlueWalletRelease.entitlements */,
@ -501,8 +501,6 @@
32B5A3292334450100F8D608 /* Bridge.swift */,
32B5A3282334450100F8D608 /* BlueWallet-Bridging-Header.h */,
6DF25A9E249DB97E001D06F5 /* LaunchScreen.storyboard */,
6D32C5C42596CE2F008C077C /* EventEmitter.h */,
6D32C5C52596CE3A008C077C /* EventEmitter.m */,
84E05A832721191B001A0D3A /* Settings.bundle */,
B4742E962CCDBE8300380EEE /* Localizable.xcstrings */,
);
@ -677,6 +675,15 @@
name = Products;
sourceTree = "<group>";
};
B409AB072D71E07C00BA06F8 /* MenuElementsEmitter */ = {
isa = PBXGroup;
children = (
B409AB052D71E07500BA06F8 /* MenuElementsEmitter.swift */,
B409AB032D71DFAA00BA06F8 /* MenuElementsEmitter.m */,
);
path = MenuElementsEmitter;
sourceTree = "<group>";
};
B40D4E31225841EC00428FCC /* BlueWalletWatch */ = {
isa = PBXGroup;
children = (
@ -786,6 +793,15 @@
path = Shared;
sourceTree = "<group>";
};
B44305BD2D6A04B9004675CC /* SegmentedControl */ = {
isa = PBXGroup;
children = (
B44305BB2D6A04B2004675CC /* CustomSegmentedControl.m */,
B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */,
);
path = SegmentedControl;
sourceTree = "<group>";
};
B450109A2C0FCD7E00619044 /* Utilities */ = {
isa = PBXGroup;
children = (
@ -798,21 +814,14 @@
B45010A12C1504E900619044 /* Components */ = {
isa = PBXGroup;
children = (
B409AB072D71E07C00BA06F8 /* MenuElementsEmitter */,
B44305BD2D6A04B9004675CC /* SegmentedControl */,
B4B3EC232D69FF8700327F3D /* EventEmitter.swift */,
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */,
B45010A82C1507F000619044 /* SegmentedControl */,
);
path = Components;
sourceTree = "<group>";
};
B45010A82C1507F000619044 /* SegmentedControl */ = {
isa = PBXGroup;
children = (
B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */,
B45010A92C15080500619044 /* CustomSegmentedControlManager.h */,
);
path = SegmentedControl;
sourceTree = "<group>";
};
B4549F2E2B80FEA1002E3153 /* ci_scripts */ = {
isa = PBXGroup;
children = (
@ -829,15 +838,6 @@
path = BlueWalletUITests;
sourceTree = "<group>";
};
B4C0752B2CDDB3CC00322A84 /* MenuElementsEmitter */ = {
isa = PBXGroup;
children = (
B4C075292CDDB3C500322A84 /* MenuElementsEmitter.m */,
B4C075282CDDB3BE00322A84 /* MenuElementsEmitter.h */,
);
path = MenuElementsEmitter;
sourceTree = "<group>";
};
FAA856B639C61E61D2CF90A8 /* Pods */ = {
isa = PBXGroup;
children = (
@ -993,6 +993,7 @@
};
};
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "BlueWallet" */;
compatibilityVersion = "Xcode 15.3";
developmentRegion = en_US;
hasScannedForEncodings = 0;
knownRegions = (
@ -1029,7 +1030,6 @@
6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */,
B41B76832B66B2FF002C48D5 /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
projectDirPath = "";
projectRoot = "";
@ -1239,28 +1239,30 @@
B44033EE2BCC374500162242 /* Numeric+abbreviated.swift in Sources */,
B48630E82CCEE92400A8425C /* PriceWidget.swift in Sources */,
B44033DD2BCC36C300162242 /* LatestTransaction.swift in Sources */,
6D32C5C62596CE3A008C077C /* EventEmitter.m in Sources */,
B49A28C12CD199FC006B08E4 /* SwiftTCPClient.swift in Sources */,
B44033FE2BCC37D700162242 /* MarketAPI.swift in Sources */,
B450109C2C0FCD8A00619044 /* Utilities.swift in Sources */,
B409AB042D71DFAA00BA06F8 /* MenuElementsEmitter.m in Sources */,
B48630E52CCEE8B800A8425C /* PriceView.swift in Sources */,
B48630E72CCEE91900A8425C /* PriceWidgetProvider.swift in Sources */,
B4B3EC252D69FF8700327F3D /* EventEmitter.swift in Sources */,
B49A28C02CD199C7006B08E4 /* MarketAPI+Electrum.swift in Sources */,
B48630ED2CCEEEB000A8425C /* WalletAppShortcuts.swift in Sources */,
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */,
B409AB062D71E07500BA06F8 /* MenuElementsEmitter.swift in Sources */,
B44033CE2BCC352900162242 /* UserDefaultsGroup.swift in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
B45942C42CDECF2400B3DC2E /* MenuElementsEmitter.m in Sources */,
B461B852299599F800E431AA /* AppDelegate.mm in Sources */,
B44033F42BCC377F00162242 /* WidgetData.swift in Sources */,
B49A28C52CD1A894006B08E4 /* MarketData.swift in Sources */,
B49A28BF2CD18A9A006B08E4 /* FiatUnitEnum.swift in Sources */,
B44305BC2D6A04B2004675CC /* CustomSegmentedControl.m in Sources */,
B44033C42BCC332400162242 /* Balance.swift in Sources */,
B48630EE2CCEEEE900A8425C /* PriceIntent.swift in Sources */,
B44034072BCC38A000162242 /* FiatUnit.swift in Sources */,
B44034002BCC37F800162242 /* Bundle+decode.swift in Sources */,
B44033E22BCC36CB00162242 /* Placeholders.swift in Sources */,
B4793DBB2CEDACBD00C92C2E /* Chain.swift in Sources */,
B4B3EC222D69FF6C00327F3D /* CustomSegmentedControl.swift in Sources */,
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */,
B48630E12CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */,
B44033DA2BCC369A00162242 /* Colors.swift in Sources */,
@ -1453,7 +1455,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703136999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
@ -1471,7 +1473,7 @@
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1481,7 +1483,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.0.8;
MARKETING_VERSION = 7.1.5;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -1516,7 +1518,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703136999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
@ -1529,7 +1531,7 @@
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1539,7 +1541,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.0.8;
MARKETING_VERSION = 7.1.5;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -1575,20 +1577,20 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703136999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = Stickers/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LIBRARY_SEARCH_PATHS = (
"$(SDKROOT)/usr/lib/swift",
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.0.8;
MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1618,20 +1620,20 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703136999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = Stickers/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LIBRARY_SEARCH_PATHS = (
"$(SDKROOT)/usr/lib/swift",
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.0.8;
MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers;
@ -1662,7 +1664,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703136999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
@ -1681,7 +1683,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.0.8;
MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1718,7 +1720,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703136999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
@ -1737,7 +1739,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.0.8;
MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget;
@ -1905,7 +1907,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703136999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
@ -1925,7 +1927,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.0.8;
MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1958,7 +1960,7 @@
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703136999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
@ -1978,7 +1980,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.0.8;
MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension;
@ -2010,7 +2012,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703136999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
@ -2024,7 +2026,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.0.8;
MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -2059,7 +2061,7 @@
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703136999;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
@ -2073,7 +2075,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.0.8;
MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch;

View file

@ -6,12 +6,9 @@
#import "RNQuickActionManager.h"
#import <UserNotifications/UserNotifications.h>
#import <RNCPushNotificationIOS.h>
#import "EventEmitter.h"
#import "MenuElementsEmitter.h"
#import <React/RCTRootView.h>
#import <Bugsnag/Bugsnag.h>
#import "BlueWallet-Swift.h"
#import "CustomSegmentedControlManager.h"
@interface AppDelegate() <UNUserNotificationCenterDelegate>
@ -23,8 +20,6 @@
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[MenuElementsEmitter sharedInstance];
[CustomSegmentedControlManager registerIfNecessary];
[self clearFilesIfNeeded];
self.userDefaultsGroup = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"];
@ -154,27 +149,42 @@
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity
restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
NSDictionary *userActivityData = @{@"activityType": userActivity.activityType, @"userInfo": userActivity.userInfo};
// Validate userActivity and its type
if (!userActivity || !userActivity.activityType) {
NSLog(@"[Handoff] Invalid or missing userActivity");
return NO;
}
NSDictionary *userActivityData = @{@"activityType": userActivity.activityType ?: @"",
@"userInfo": userActivity.userInfo ?: @{}};
// Save activity data to userDefaults for potential later use
[self.userDefaultsGroup setValue:userActivityData forKey:@"onUserActivityOpen"];
// Check if the activity type matches the allowed types
// Check if the activity type matches one of the allowed types
if ([userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.receiveonchain"] ||
[userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.xpub"] ||
[userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.blockexplorer"]) {
[EventEmitter.sharedInstance sendUserActivity:userActivityData];
if ([EventEmitter.shared respondsToSelector:@selector(sendUserActivity:)]) {
[EventEmitter.shared sendUserActivity:userActivityData];
} else {
NSLog(@"[Handoff] EventEmitter does not implement sendUserActivity:");
}
return YES;
}
if (userActivity.activityType == NSUserActivityTypeBrowsingWeb) {
// Forward web browsing activities to LinkingManager
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
// If activity type does not match any of the specified types, do nothing
NSLog(@"[Handoff] Unhandled user activity type: %@", userActivity.activityType);
return NO;
}
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [RCTLinkingManager application:app openURL:url options:options];
}
@ -195,7 +205,7 @@
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
NSDictionary *userInfo = notification.request.content.userInfo;
completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionBadge);
}
- (void)buildMenuWithBuilder:(id<UIMenuBuilder>)builder {
@ -244,25 +254,59 @@
}
- (void)openSettings:(UIKeyCommand *)keyCommand {
[MenuElementsEmitter.sharedInstance openSettings];
// Safely access the MenuElementsEmitter
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
if (emitter) {
NSLog(@"[MenuElements] AppDelegate: openSettings called, calling emitter");
// Force on main thread for consistency
dispatch_async(dispatch_get_main_queue(), ^{
[emitter openSettings];
});
} else {
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for openSettings");
}
}
- (void)addWalletAction:(UIKeyCommand *)keyCommand {
// Implement the functionality for adding a wallet
[MenuElementsEmitter.sharedInstance addWalletMenuAction];
NSLog(@"Add Wallet action performed");
// Safely access the MenuElementsEmitter
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
if (emitter) {
NSLog(@"[MenuElements] AppDelegate: addWalletAction called, calling emitter");
// Force on main thread for consistency
dispatch_async(dispatch_get_main_queue(), ^{
[emitter addWalletMenuAction];
});
} else {
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for addWalletAction");
}
}
- (void)importWalletAction:(UIKeyCommand *)keyCommand {
// Implement the functionality for adding a wallet
[MenuElementsEmitter.sharedInstance importWalletMenuAction];
NSLog(@"Import Wallet action performed");
// Safely access the MenuElementsEmitter
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
if (emitter) {
NSLog(@"[MenuElements] AppDelegate: importWalletAction called, calling emitter");
// Force on main thread for consistency
dispatch_async(dispatch_get_main_queue(), ^{
[emitter importWalletMenuAction];
});
} else {
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for importWalletAction");
}
}
- (void)reloadTransactionsAction:(UIKeyCommand *)keyCommand {
// Implement the functionality for adding a wallet
[MenuElementsEmitter.sharedInstance reloadTransactionsMenuAction];
NSLog(@"Reload Transactions action performed");
// Safely access the MenuElementsEmitter
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
if (emitter) {
NSLog(@"[MenuElements] AppDelegate: reloadTransactionsAction called, calling emitter");
// Force on main thread for consistency
dispatch_async(dispatch_get_main_queue(), ^{
[emitter reloadTransactionsMenuAction];
});
} else {
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for reloadTransactionsAction");
}
}
- (void)showHelp:(id)sender {

View file

@ -0,0 +1,32 @@
import Foundation
import React
@objc(EventEmitter)
class EventEmitter: RCTEventEmitter {
static let sharedInstance = EventEmitter()
override class func requiresMainQueueSetup() -> Bool {
return true
}
@objc static func shared() -> EventEmitter {
return sharedInstance
}
override func supportedEvents() -> [String]! {
return ["onUserActivityOpen"]
}
@objc func sendUserActivity(_ userInfo: [String: Any]) {
sendEvent(withName: "onUserActivityOpen", body: userInfo)
}
@objc func getMostRecentUserActivity(_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock) {
if let defaults = UserDefaults(suiteName: "group.io.bluewallet.bluewallet") {
resolve(defaults.value(forKey: "onUserActivityOpen"))
} else {
resolve(nil)
}
}
}

View file

@ -0,0 +1,9 @@
#import <React/RCTViewManager.h>
@interface RCT_EXTERN_MODULE(CustomSegmentedControlManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(values, NSArray)
RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(onChangeEvent, RCTDirectEventBlock)
@end

View file

@ -0,0 +1,70 @@
import UIKit
import React
class CustomSegmentedControl: UISegmentedControl {
@objc var onChangeEvent: RCTDirectEventBlock?
override init(frame: CGRect) {
super.init(frame: frame)
addTarget(self, action: #selector(onChange(_:)), for: .valueChanged)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addTarget(self, action: #selector(onChange(_:)), for: .valueChanged)
}
@objc func setValues(_ values: [String]) {
removeAllSegments()
for (index, title) in values.enumerated() {
insertSegment(withTitle: title, at: index, animated: false)
}
}
@objc func setSelectedIndex(_ selectedIndex: NSNumber) {
self.selectedSegmentIndex = selectedIndex.intValue
}
@objc func onChange(_ sender: UISegmentedControl) {
onChangeEvent?(["selectedIndex": sender.selectedSegmentIndex])
}
}
@objc(CustomSegmentedControlManager)
class CustomSegmentedControlManager: RCTViewManager {
static var isRegistered = false
override func view() -> UIView! {
// Ensure native module is registered before returning the view.
CustomSegmentedControlManager.registerIfNecessary()
return CustomSegmentedControl(frame: .zero)
}
@objc static func registerIfNecessary() {
if !isRegistered {
isRegistered = true
}
}
// Changed from static to instance method.
override func constantsToExport() -> [AnyHashable: Any]! {
return [
"bubblingEventTypes": [
"onChangeEvent": [
"phasedRegistrationNames": [
"bubbled": "onChangeEvent",
"captured": "onChangeEventCapture"
]
]
]
]
}
override class func moduleName() -> String! {
return "CustomSegmentedControl"
}
override class func requiresMainQueueSetup() -> Bool {
return true
}
}

View file

@ -1,15 +0,0 @@
//
// SegmentedControlManager.h
// BlueWallet
//
// Created by Marcos Rodriguez on 6/8/24.
// Copyright © 2024 BlueWallet. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface CustomSegmentedControlManager : RCTViewManager
+ (void)registerIfNecessary;
@end

View file

@ -1,70 +0,0 @@
#import "CustomSegmentedControlManager.h"
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcher.h>
#import <React/UIView+React.h>
@interface CustomSegmentedControl : UISegmentedControl
@property (nonatomic, copy) RCTDirectEventBlock onChangeEvent;
- (void)setValues:(NSArray<NSString *> *)values;
- (void)setSelectedIndex:(NSNumber *)selectedIndex;
@end
@implementation CustomSegmentedControl
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged];
}
return self;
}
- (void)setValues:(NSArray<NSString *> *)values {
@try {
[self removeAllSegments];
for (NSUInteger i = 0; i < values.count; i++) {
[self insertSegmentWithTitle:values[i] atIndex:i animated:NO];
}
} @catch (NSException *exception) {
NSLog(@"Error setting property 'values': %@", exception.reason);
}
}
- (void)setSelectedIndex:(NSNumber *)selectedIndex {
@try {
self.selectedSegmentIndex = selectedIndex.integerValue;
} @catch (NSException *exception) {
NSLog(@"Error setting property 'selectedIndex': %@", exception.reason);
}
}
- (void)onChange:(UISegmentedControl *)sender {
if (self.onChangeEvent) {
self.onChangeEvent(@{@"selectedIndex": @(self.selectedSegmentIndex)});
}
}
@end
@implementation CustomSegmentedControlManager
static BOOL isRegistered = NO;
RCT_EXPORT_MODULE(CustomSegmentedControl)
- (UIView *)view {
return [CustomSegmentedControl new];
}
RCT_EXPORT_VIEW_PROPERTY(values, NSArray)
RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(onChangeEvent, RCTDirectEventBlock)
+ (void)registerIfNecessary {
if (!isRegistered) {
isRegistered = YES;
// Registration logic if necessary
}
}
@end

View file

@ -1,17 +0,0 @@
//
// EventEmitter.h
// BlueWallet
//
// Created by Marcos Rodriguez on 12/25/20.
// Copyright © 2020 BlueWallet. All rights reserved.
//
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface EventEmitter : RCTEventEmitter <RCTBridgeModule>
+ (EventEmitter *)sharedInstance;
- (void)sendUserActivity:(NSDictionary *)userInfo;
@end

View file

@ -1,54 +0,0 @@
//
// EventEmitter.m
// BlueWallet
//
// Created by Marcos Rodriguez on 12/25/20.
// Copyright © 2020 BlueWallet. All rights reserved.
//
#import "EventEmitter.h"
static EventEmitter *sharedInstance;
@implementation EventEmitter
RCT_EXPORT_MODULE();
+ (BOOL)requiresMainQueueSetup {
return YES;
}
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (void)removeListeners:(double)count {
}
- (instancetype)init {
self = [super init];
return self;
}
- (NSArray<NSString *> *)supportedEvents {
return @[@"onUserActivityOpen"];
}
- (void)sendUserActivity:(NSDictionary *)userInfo
{
[self sendEventWithName:@"onUserActivityOpen" body:userInfo];
}
RCT_EXPORT_METHOD(getMostRecentUserActivity:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"];
resolve([defaults valueForKey:@"onUserActivityOpen"]);
}
@end

33
ios/EventEmitter.swift Normal file
View file

@ -0,0 +1,33 @@
import Foundation
import React
@objc(EventEmitter)
class EventEmitter: RCTEventEmitter {
static let sharedInstance = EventEmitter()
override class func requiresMainQueueSetup() -> Bool {
return true
}
@objc static func shared() -> EventEmitter {
return sharedInstance
}
override func supportedEvents() -> [String]! {
return ["onUserActivityOpen"]
}
@objc func sendUserActivity(_ userInfo: [String: Any]) {
// Removed unnecessary type check; directly sending the event with userInfo.
sendEvent(withName: "onUserActivityOpen", body: userInfo)
}
@objc func getMostRecentUserActivity(_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock) {
if let defaults = UserDefaults(suiteName: "group.io.bluewallet.bluewallet") {
resolve(defaults.value(forKey: "onUserActivityOpen"))
} else {
resolve(nil)
}
}
}

View file

@ -1,17 +0,0 @@
//
// MenuElementsEmitter.h
// BlueWallet
//
#import <React/RCTEventEmitter.h>
@interface MenuElementsEmitter : RCTEventEmitter
+ (instancetype)sharedInstance;
- (void)openSettings;
- (void)addWalletMenuAction;
- (void)importWalletMenuAction;
- (void)reloadTransactionsMenuAction;
@end

View file

@ -1,27 +0,0 @@
//
// MenuElementsEmitter.h
// BlueWallet
//
// Created by Marcos Rodriguez on 11/7/24.
// Copyright © 2024 BlueWallet. All rights reserved.
//
//
// MenuElementsEmitter.h
// BlueWallet
//
#import <React/RCTEventEmitter.h>
@interface MenuElementsEmitter : RCTEventEmitter
+ (instancetype)sharedInstance;
- (void)openSettings;
- (void)addWalletMenuAction;
- (void)importWalletMenuAction;
- (void)reloadTransactionsMenuAction;
- (NSArray<NSString *> *)supportedEvents;
@end

View file

@ -1,62 +1,13 @@
//
// MenuElementsEmitter.m
// BlueWallet
//
// Created by Marcos Rodriguez on 11/7/24.
// Copyright © 2024 BlueWallet. All rights reserved.
//
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
#import "MenuElementsEmitter.h"
@interface RCT_EXTERN_MODULE(MenuElementsEmitter, RCTEventEmitter)
static MenuElementsEmitter *sharedInstance;
@implementation MenuElementsEmitter
RCT_EXPORT_MODULE();
+ (BOOL)requiresMainQueueSetup {
return YES;
}
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (instancetype)init {
self = [super init];
if (self) {
sharedInstance = self;
}
return self;
}
- (NSArray<NSString *> *)supportedEvents {
return @[
@"openSettings",
@"addWalletMenuAction",
@"importWalletMenuAction",
@"reloadTransactionsMenuAction"
];
}
- (void)openSettings {
[self sendEventWithName:@"openSettings" body:nil];
}
- (void)addWalletMenuAction {
[self sendEventWithName:@"addWalletMenuAction" body:nil];
}
- (void)importWalletMenuAction {
[self sendEventWithName:@"importWalletMenuAction" body:nil];
}
- (void)reloadTransactionsMenuAction {
[self sendEventWithName:@"reloadTransactionsMenuAction" body:nil];
}
RCT_EXTERN_METHOD(supportedEvents)
RCT_EXTERN_METHOD(openSettings)
RCT_EXTERN_METHOD(addWalletMenuAction)
RCT_EXTERN_METHOD(importWalletMenuAction)
RCT_EXTERN_METHOD(reloadTransactionsMenuAction)
RCT_EXTERN_METHOD(shared)
@end

View file

@ -0,0 +1,134 @@
import Foundation
import React
@objc(MenuElementsEmitter)
class MenuElementsEmitter: RCTEventEmitter {
// Use a weak reference for the singleton to prevent retain cycles
private static weak var sharedInstance: MenuElementsEmitter?
// Use LRU cache with a max size to prevent unbounded growth
private var lastEventTime: [String: TimeInterval] = [:]
private let throttleInterval: TimeInterval = 0.3 // 300ms throttle
private let maxCacheSize = 10 // Limit the cache size
// Track listener state without needing constant bridge access
private var hasListeners = false
override init() {
super.init()
MenuElementsEmitter.sharedInstance = self
NSLog("[MenuElements] MenuElementsEmitter initialized")
}
deinit {
NSLog("[MenuElements] MenuElementsEmitter deallocated")
// Ensure all event listeners are removed in deinit
self.removeAllListeners()
}
override class func requiresMainQueueSetup() -> Bool {
return true
}
override func supportedEvents() -> [String]! {
return ["openSettings", "addWalletMenuAction", "importWalletMenuAction", "reloadTransactionsMenuAction"]
}
@objc static func shared() -> MenuElementsEmitter? {
if sharedInstance == nil {
NSLog("[MenuElements] Warning: Attempting to use sharedInstance when it's nil")
}
return sharedInstance
}
override func startObserving() {
hasListeners = true
NSLog("[MenuElements] Started observing events, bridge: \(self.bridge != nil ? "available" : "unavailable")")
}
override func stopObserving() {
hasListeners = false
NSLog("[MenuElements] Stopped observing events")
// Clear cache when stopping observation
lastEventTime.removeAll()
}
private func limitCacheSize() {
if lastEventTime.count > maxCacheSize {
// Remove oldest entries if cache is too large
let sortedKeys = lastEventTime.sorted(by: { $0.value < $1.value })
for i in 0..<(lastEventTime.count - maxCacheSize) {
lastEventTime.removeValue(forKey: sortedKeys[i].key)
}
}
}
private func canEmitEvent(named eventName: String) -> Bool {
let now = Date().timeIntervalSince1970
if let lastTime = lastEventTime[eventName], now - lastTime < throttleInterval {
NSLog("[MenuElements] Throttling event: \(eventName)")
return false
}
lastEventTime[eventName] = now
limitCacheSize() // Keep cache size in check
let canEmit = hasListeners && bridge != nil
if (!canEmit) {
NSLog("[MenuElements] Cannot emit event: \(eventName), hasListeners: \(hasListeners), bridge: \(bridge != nil ? "available" : "unavailable")")
}
return canEmit
}
private func safelyEmitEvent(withName name: String) {
guard canEmitEvent(named: name) else { return }
NSLog("[MenuElements] Emitting event: \(name)")
// Use weak self to avoid retain cycles
DispatchQueue.main.async { [weak self] in
guard let self = self, self.bridge != nil, self.hasListeners else {
NSLog("[MenuElements] Failed to emit event: \(name) - bridge or listeners not available")
return
}
self.sendEvent(withName: name, body: nil)
NSLog("[MenuElements] Event sent: \(name)")
}
}
func removeAllListeners() {
NSLog("[MenuElements] Removing all listeners")
// Clean up resources
lastEventTime.removeAll()
}
@objc func openSettings() {
NSLog("[MenuElements] openSettings method called")
safelyEmitEvent(withName: "openSettings")
}
@objc func addWalletMenuAction() {
NSLog("[MenuElements] addWalletMenuAction method called")
safelyEmitEvent(withName: "addWalletMenuAction")
}
@objc func importWalletMenuAction() {
NSLog("[MenuElements] importWalletMenuAction method called")
safelyEmitEvent(withName: "importWalletMenuAction")
}
@objc func reloadTransactionsMenuAction() {
safelyEmitEvent(withName: "reloadTransactionsMenuAction")
}
override func invalidate() {
NSLog("[MenuElements] Module invalidated")
if MenuElementsEmitter.sharedInstance === self {
MenuElementsEmitter.sharedInstance = nil
}
removeAllListeners()
super.invalidate()
}
}

View file

@ -6,8 +6,8 @@ def node_require(script)
{paths: [process.argv[1]]},
)", __dir__]).strip
end
min_ios_version_supported = '14.0'
ENV['RCT_NEW_ARCH_ENABLED'] = '0'
min_ios_version_supported = '15.1'
node_require('react-native/scripts/react_native_pods.rb')
node_require('react-native-permissions/scripts/setup.rb')
@ -54,7 +54,7 @@ post_install do |installer|
)
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.1'
if ['React-Core-AccessibilityResources'].include? target.name
config.build_settings['CODE_SIGN_STYLE'] = "Manual"

File diff suppressed because it is too large Load diff

View file

@ -30,8 +30,8 @@ class MarketAPI {
return "https://www.bnr.ro/nbrfxrates.xml"
case "Kraken":
return "https://api.kraken.com/0/public/Ticker?pair=XXBTZ\(endPointKey.uppercased())"
default:
return "https://api.coindesk.com/v1/bpi/currentprice/\(endPointKey).json"
default: // CoinDesk
return "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=\(endPointKey)"
}
}
@ -131,8 +131,14 @@ class MarketAPI {
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
}
}
default:
throw CurrencyError(errorDescription: "Unsupported data source \(source)")
default: // CoinDesk
if let rateDouble = json[endPointKey] as? Double {
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
latestRateDataStore = WidgetDataStore(rate: String(rateDouble), lastUpdate: lastUpdatedString, rateDouble: rateDouble)
return latestRateDataStore
} else {
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
}
}
}

View file

@ -12,7 +12,6 @@
"storage_is_encrypted": "وحدة التخزين مشفرة. أنت بحاجة إلى كلمة المرور لفك تشفيرها",
"yes": "نعم",
"no": "لا",
"save": "حفظ",
"seed": "عبارة الاسترداد",
"success": "نجاح",
"wallet_key": "مفتاح المحفظة",
@ -210,13 +209,8 @@
"set_electrum_server_as_default": "هل تريد تعيين {server} كخادم Electrum الافتراضي؟",
"electrum_settings_server": "خادم Electrum",
"electrum_status": "الحالة",
"electrum_clear_alert_title": "محو السجل؟",
"electrum_clear_alert_message": "هل تريد مسح سجل خوادم Electrum؟",
"electrum_clear_alert_cancel": "الغاء",
"electrum_clear_alert_ok": "موافق",
"electrum_reset": "إعادة تعيين إلى الافتراضي",
"electrum_unable_to_connect": "تعذر الاتصال بـ {server}.",
"electrum_reset_to_default": "هل أنت متأكد من رغبتك في إعادة تعيين إعدادات Electrum إلى الإعدادات الافتراضية؟",
"electrum_reset": "إعادة تعيين إلى الافتراضي",
"encrypt_decrypt": "فك تشفير وحدة التخزين",
"encrypt_decrypt_q": "هل أنت متأكد أنك تريد فك تشفير وحدة التخزين الخاصة بك؟ سيسمح إجراء ذلك بالوصول إلى محافظك دون كلمة مرور.",
"encrypt_enc_and_pass": "مشفرة ومحمية بكلمة مرور",
@ -337,7 +331,6 @@
"details_export_history": "تصدير السجل ل ملف CSV",
"details_master_fingerprint": "البصمة الرئيسية",
"details_multisig_type": "متعدد التواقيع",
"details_no_cancel": "لا، إلغاء",
"details_show_xpub": "إظهار عنوان XPUB للمحفظة",
"details_show_addresses": "عرض العناوين",
"details_title": "المحفظة",

View file

@ -12,7 +12,6 @@
"storage_is_encrypted": "Ваша сховішча зашыфравана. Для расшыфроўкі патрабуецца пароль.",
"yes": "Так",
"no": "Не",
"save": "Захаваць",
"seed": "Семя",
"success": "Посьпех",
"wallet_key": "Ключ ад кашалька"
@ -44,7 +43,6 @@
"create_to": "Да"
},
"settings": {
"electrum_clear_alert_cancel": "Адмяніць",
"save": "Захаваць"
},
"wallets": {

View file

@ -11,7 +11,6 @@
"storage_is_encrypted": "Вашият портфейл е криптиран. Необходима е парола за декриптиране",
"yes": "Да",
"no": "Не",
"save": "Запази",
"seed": "Сиид",
"success": "Успех",
"wallet_key": "Парола на портфейла"
@ -168,13 +167,8 @@
"electrum_saved": "Промените бяха запазени успешно. Моля, рестартирайте Блу Уолет за да видите промените.",
"set_electrum_server_as_default": "Задайте {server} като Електрум сървър по подразбиране? ",
"electrum_status": "Статус",
"electrum_clear_alert_title": "Изчисти историята?",
"electrum_clear_alert_message": "Искате ли да изтриете електрум сървър историята?",
"electrum_clear_alert_cancel": "Отказ",
"electrum_clear_alert_ok": "Ок",
"electrum_reset": "Начални настройки",
"electrum_unable_to_connect": "Не възможно свързване със сървър {server}.",
"electrum_reset_to_default": "Сигурни ли сте, че искате да върнете Електрум към първоначалните настройки?",
"electrum_reset": "Начални настройки",
"encrypt_decrypt": "Декриптирай хранилището",
"encrypt_decrypt_q": "Сигурни ли сте, че искате да декриптирате хранилището? Това ще направи портфейлите ви достъпни без парола.",
"encrypt_enc_and_pass": "Криптиран и защитен с парола",

View file

@ -11,7 +11,6 @@
"storage_is_encrypted": "جاگه زفت کردنی ایسا ریس رزم هڌ. سی گۊشیڌنس وا رزمسه داشته بۊی.",
"yes": "هری",
"no": "نه",
"save": "زفت کردن",
"seed": "سید",
"success": "سر ٱنجوم گرهڌ",
"wallet_key": "کیلیت کیف پیل",
@ -187,13 +186,8 @@
"set_electrum_server_as_default": "{server} سی سرور پؽش فرز الکترام ساموو بۊوه؟",
"electrum_settings_server": "سرور الکترام",
"electrum_status": "وزیت",
"electrum_clear_alert_title": "ویرگار پاک بۊ؟",
"electrum_clear_alert_message": "اخۊی ویرگار سرورا الکترام نه پاک کۊنی؟",
"electrum_clear_alert_cancel": "لقو",
"electrum_clear_alert_ok": "هری",
"electrum_reset": "ورگندن به پؽش فرز",
"electrum_unable_to_connect": "نا مۉفق منه منپیز به {server}",
"electrum_reset_to_default": "الن اخۊی سامووا الکترام نه به هالت پؽش فرز وورنشۊوی کۊنی؟",
"electrum_reset": "ورگندن به پؽش فرز",
"encrypt_decrypt": "رزم گوشایی جاگه زفت کردنی",
"encrypt_title": "امنیت",
"encrypt_tstorage": "جاگه زفت کردنی",

View file

@ -12,7 +12,6 @@
"storage_is_encrypted": "L'informació està xifrada. Es requereix la contrasenya per a desxifrar-la.",
"yes": "Si",
"no": "No",
"save": "Desar",
"seed": "Llavor",
"success": "Èxit",
"wallet_key": "Clau del moneder",
@ -168,9 +167,6 @@
"use_ssl": "Utilitza SSL",
"electrum_settings_server": "Servidor Electrum",
"electrum_status": "estat",
"electrum_clear_alert_title": "Netejar l'historial?",
"electrum_clear_alert_cancel": "Cancel·lar",
"electrum_clear_alert_ok": "D'acord",
"electrum_reset": "Restableix la configuració predeterminada",
"encrypt_title": "Seguretat",
"encrypt_use": "Utilitza {type}",
@ -237,7 +233,6 @@
"details_delete_wallet": "Eliminar Moneder",
"details_export_backup": "Exportar / Guardar",
"details_master_fingerprint": "Petjada digital mestre",
"details_no_cancel": "No, cancel·lar",
"details_show_xpub": "Mostrar wallet XPUB",
"details_title": "Detalls del moneder",
"wallets": "moneders",

View file

@ -10,12 +10,11 @@
"never": "Nikdy",
"of": "{number} z(e) {total}",
"ok": "OK",
"customize": "Přizpůsobit",
"enter_url": "Zadejte URL",
"storage_is_encrypted": "Vaše úložiště je zašifrované. Zadejte heslo k odemčení.",
"yes": "Ano",
"no": "Ne",
"save": "Uložit",
"save": "Uložit",
"seed": "Seed",
"success": "Úspěch",
"wallet_key": "Klíč peněženky",
@ -28,6 +27,8 @@
"enter_amount": "Zadejte částku",
"qr_custom_input_button": "Klepněte 10× k zadání vlastního vstupu",
"unlock": "Odemknout",
"port": "Port",
"ssl_port": "SSL port",
"suggested": "Doporučené"
},
"azteco": {
@ -74,6 +75,7 @@
"please_pay": "Zaplaťte prosím",
"preimage": "Předobraz",
"sats": "sats.",
"date_time": "Datum a čas",
"wasnt_paid_and_expired": "Tato faktura nebyla zaplacena a její platnost vypršela."
},
"plausibledeniability": {
@ -106,7 +108,7 @@
"minSats": "Minimální množství je {min} sats",
"minSatsFull": "Minimální částka je {min} sats nebo {currency} ",
"qrcode_for_the_address": "QR kód pro adresu",
"bip47_explanation": "Platební kódy jsou univerzální adresou, která zabraňuje odhalení adres vaší peněženky. Nejsou podporovány všemi službami."
"bip47_explanation": "Platební kódy jsou univerzální adresy, které zabraňují prozrazení adres vaší peněženky. Ne všechny služby je však podporují."
},
"send": {
"provided_address_is_invoice": "Zdá se, že tato adresa je určena pro Lightning fakturu. Přejděte prosím do své Lightning peněženky, abyste mohli provést platbu této faktury.",
@ -134,6 +136,9 @@
"details_add_recc_rem_all_alert_description": "Jste si jisti, že chcete odebrat všechny příjemce?",
"details_add_rec_rem_all": "Odebrat všechny příjemce",
"details_recipients_title": "Příjemci",
"details_recipient_title": "Příjemce č. {number} z(e) {total}",
"please_complete_recipient_title": "Nekompletní příjemce",
"please_complete_recipient_details": "Vyplňte prosím všechny detaily příjemce č. {number} před přidáním nového příjemce.",
"details_address": "Adresa",
"details_address_field_is_not_valid": "Adresa není správně vyplněna.",
"details_adv_fee_bump": "Povolit navýšení poplatku",
@ -180,6 +185,7 @@
"input_total": "Celkem:",
"permission_camera_message": "K použití fotoaparátu potřebujeme vaše povolení",
"psbt_sign": "Podepsat transakci",
"invalid_psbt": "Byla poskytnuta neplatná PSBT.",
"open_settings": "Otevřít nastavení",
"permission_storage_denied_message": "BlueWallet nemůže tento soubor uložit. Otevřete prosím nastavení zařízení a povolte funkci Oprávnění k ukládání.",
"permission_storage_title": "Povolení k přístupu do úložiště",
@ -190,7 +196,7 @@
"outdated_rate": "Kurz byl naposledy aktualizován: {date}",
"psbt_tx_open": "Otevřít podepsanou transakci",
"psbt_tx_scan": "Skenovat podepsanou transakci",
"qr_error_no_qrcode": "Ve vybraném obrázku se nám nepodařilo najít QR kód. Ujistěte se, že obrázek obsahuje pouze QR kód a žádný další obsah, například text nebo tlačítka.",
"qr_error_no_qrcode": "Ve vybraném obrázku nebyl nalezen platný QR kód. Zajistěte, aby obrázek obsahoval pouze QR kód a žádný další obsah jako třeba text nebo tlačítka.",
"reset_amount": "Vynulovat částku",
"reset_amount_confirm": "Chcete částku vynulovat?",
"success_done": "Hotovo",
@ -250,15 +256,11 @@
"electrum_status": "Stav",
"electrum_preferred_server": "Upřednostňovaný server",
"electrum_preferred_server_description": "Zadejte server, který má vaše peněženka používat pro všechny bitcoinové aktivity. Po nastavení bude vaše peněženka používat výhradně tento server ke kontrole zůstatků, odesílání transakcí a načítání síťových dat. Před nastavením se ujistěte, že tomuto serveru důvěřujete.",
"electrum_clear_alert_title": "Smazat historii?",
"electrum_clear_alert_message": "Chcete vymazat historii serverů Electrum?",
"electrum_clear_alert_cancel": "Zrušit",
"electrum_clear_alert_ok": "Ok",
"electrum_reset": "Obnovit do výchozího nastavení",
"electrum_unable_to_connect": "Nelze se připojit k {server}.",
"electrum_history": "Historie",
"electrum_reset_to_default": "Opravdu chcete obnovit nastavení Electrum na výchozí hodnoty?",
"electrum_clear": "Vymazat historii",
"electrum_reset_to_default": "Toto nastavení nechá aplikaci BlueWallet náhodně vybrat server ze seznamu navrhovaných.",
"electrum_reset": "Obnovit do výchozího nastavení",
"electrum_reset_to_default_and_clear_history": "Obnovit výchozí nastavení a vymazat historii",
"encrypt_decrypt": "Dešifrovat úložiště",
"encrypt_decrypt_q": "Opravdu chcete dešifrovat úložiště? To umožní přistupovat k vašim peněženkám bez hesla.",
"encrypt_enc_and_pass": "Šifrovat a chránit heslem",
@ -272,6 +274,8 @@
"encrypt_title": "Zabezpečení",
"encrypt_tstorage": "Úložiště",
"encrypt_use": "Použít {type}",
"set_as_preferred": "Nastavit jako preferovaný",
"set_as_preferred_electrum": "Nastavení {host}:{port} jako upřednostňovaného serveru zakáže náhodné připojování k navrhovanému serveru.",
"encrypted_feature_disabled": "Tato funkce nemůže být použita, pokud je povoleno zašifrované úložiště.",
"encrypt_use_expl": "{type} bude použit k potvrzení vaší identity před provedením transakce, odemknutím, exportem nebo smazáním peněženky. {type} nebude použit k odemknutí zašifrovaného úložiště.",
"biometrics_fail": "Pokud {type} není povolen, nebo selže při odemykání, můžete jako alternativu použít přístupový kód vašeho zařízení.",
@ -291,6 +295,7 @@
"network": "Síť",
"network_broadcast": "Odeslat transakci",
"network_electrum": "Electrum server",
"electrum_suggested_description": "Pokud nebude nastaven preferovaný server, bude náhodně vybrán navrhovaný server.",
"not_a_valid_uri": "Neplatná URI",
"notifications": "Oznámení",
"open_link_in_explorer": "Otevřít odkaz v průzkumníku",
@ -402,7 +407,6 @@
"add_wallet_name": "Název peněženky",
"add_wallet_type": "Typ",
"add_wallet_seed_length": "Délka seedu",
"add_wallet_seed_length_message": "Zvolte délku seed fráze, kterou chcete použít pro tuto peněženku.",
"add_wallet_seed_length_12": "12 slov",
"add_wallet_seed_length_24": "24 slov",
"clipboard_bitcoin": "Ve schránce máte bitcoinovou adresu. Chcete ji použít pro transakci?",
@ -422,7 +426,6 @@
"details_export_history": "Exportovat historii do CSV",
"details_master_fingerprint": "Hlavní otisk",
"details_multisig_type": "multisig",
"details_no_cancel": "Ne, zrušit",
"details_show_xpub": "Zobrazit XPUB peněženky",
"details_show_addresses": "Zobrazit adresy",
"details_title": "Peněženka",
@ -493,7 +496,9 @@
"identity_pubkey": "Identity Pubkey",
"xpub_title": "XPUB peněženky",
"manage_wallets_search_placeholder": "Prohledat peněženky, poznámky",
"more_info": "Další informace"
"more_info": "Další informace",
"details_delete_wallet_error_message": "Došlo k problému během potvrzování, zda byla tato peněženka odstraněna z oznámení. Toto může být způsobeno problémem se sítí nebo kvůli špatnému připojení. Pokud budete pokračovat, je možné, že i nadále budete dostávat oznámení o transakcích souvisejících s touto peněženkou, přestože už bude odstraněna.",
"details_delete_anyway": "Přesto odstranit"
},
"total_balance_view": {
"display_in_bitcoin": "Zobrazit v bitcoinech",
@ -508,6 +513,10 @@
"default_label": "Vícepodpisové Úložiště",
"multisig_vault_explain": "Nejlepší zabezpečení pro velké částky",
"provide_signature": "Poskytnout podpis",
"provide_signature_details": "K podpisu této transakce použijte své zařízení a peněženku, ve které je klíč umístěn",
"provide_signature_details_bluewallet": "V aplikaci BlueWallet přejděte do nabídky Odeslat a vyberte",
"provide_signature_next_steps": "Naskenujte nebo importujte podepsanou transakci",
"provide_signature_next_steps_details": "Poté, co vaše peněženka úspěšně podepsala transakci, naskenujte uvedený QR kód, nebo importujte doprovodný soubor, a poté zkontrolujte veškeré podrobnosti transakce před tím, než ji odešlete.",
"vault_key": "{number}. klíč Úložiště",
"required_keys_out_of_total": "Požadovaných klíčů z celkového počtu",
"fee": "Poplatek: {number}",
@ -654,6 +663,8 @@
"bip47": {
"payment_code": "Platební kód",
"contacts": "Kontakty",
"bip47_explain": "Znovu použitelný a sdílitelný kód",
"bip47_explain_subtitle": "BIP47",
"purpose": "Opakovaně použitelný kód, který je možné sdílet (BIP47)",
"pay_this_contact": "Zaplatit tomuto kontaktu",
"rename_contact": "Přejmenovat kontakt",

View file

@ -11,7 +11,6 @@
"storage_is_encrypted": "Mae'r storfa wedi encryptio. Mae angen Cyfrinair i'w ddad-gryptio. ",
"yes": "Ie",
"no": "Na",
"save": "Safio",
"seed": "Hadyn",
"success": "Llwyddiant",
"wallet_key": "Allwedd waled"
@ -109,8 +108,6 @@
"default_wallets": "Gweld Pob Waled",
"electrum_connected": "Wedi Cysylltu",
"electrum_connected_not": "Heb Gysylltu",
"electrum_clear_alert_cancel": "Canslo",
"electrum_clear_alert_ok": "Iawn",
"encrypt_decrypt": "Dadgryptio'r Storfa",
"encrypt_title": "Diogelwch",
"encrypt_tstorage": "Storfa",
@ -163,7 +160,6 @@
"details_connected_to": "Wedi cysylltu efo",
"details_delete": "Gwaredu",
"details_delete_wallet": "Gwaredu Waled",
"details_no_cancel": "Na, canslo",
"details_show_addresses": "Dangos cyfeiriadau",
"wallets": "Waledi",
"details_type": "Math",

View file

@ -6,7 +6,6 @@
"never": "aldrig",
"ok": "OK",
"storage_is_encrypted": "Lageret er krypteret. Indtast adgangskode for at dekryptere",
"save": "save",
"success": "Succes"
},
"azteco": {
@ -60,7 +59,6 @@
"settings": {
"about": "Andet",
"currency": "Valuta",
"electrum_clear_alert_cancel": "Annuller",
"header": "indstillinger",
"language": "Sprog",
"lightning_settings": "Lightning settings",
@ -87,7 +85,6 @@
"details_are_you_sure": "Er du sikker?",
"details_delete": "Slet",
"details_export_backup": "Eksporter / backup",
"details_no_cancel": "Nej, annuller",
"details_show_xpub": "Vis wallet XPUB",
"details_yes_delete": "Ja, slet",
"export_title": "wallet eksport",

View file

@ -10,12 +10,11 @@
"never": "nie",
"of": "{number} von {total}",
"ok": "OK",
"customize": "Anpassen",
"enter_url": "URL eingeben",
"storage_is_encrypted": "Zum Entschlüsseln des Speichers das Passwort eingeben.",
"yes": "Ja",
"no": "Nein",
"save": "Speichern",
"save": "Speichern...",
"seed": "Seed",
"success": "Erfolg",
"wallet_key": "Wallet Schlüssel",
@ -255,8 +254,9 @@
"electrum_preferred_server_description": "Den Server eingeben, der die Wallet für alle Bitcoin-Aktivitäten verwenden soll. Sobald festgelegt, wird die Wallet um Salden abzufragen, Transaktionen zu senden und Netzwerkdaten abzurufen ausschliesslich diesen Server verwenden. Prüfen sie vorher, dass der Server vertrauenswürdig ist.",
"electrum_unable_to_connect": "Verbindung zu {server} kann nicht hergestellt werden.",
"electrum_history": "Historie",
"electrum_reset_to_default": "BlueWallet wählt zufällig einen Server aus der Vorschlagsliste oder Historie aus. Der Serververlauf bleibt dadurch unverändert.",
"electrum_reset_to_default": "Dies lässt BlueWallet zufällig einen Server aus der Liste der Server auswählen.",
"electrum_reset": "Zurücksetzten",
"electrum_reset_to_default_and_clear_history": "Auf die Standardeinstellungen zurücksetzen und den Verlauf löschen.",
"encrypt_decrypt": "Speicher entschlüsseln",
"encrypt_decrypt_q": "Die Speicherverschlüsselung wirklich aufheben? Hiermit werden die Wallet ohne Passwortschutz direkt benutzbar. ",
"encrypt_enc_and_pass": "Verschlüsselt und passwortgeschützt",
@ -403,7 +403,6 @@
"add_wallet_name": "Wallet Name",
"add_wallet_type": "Typ",
"add_wallet_seed_length": "Seedlänge",
"add_wallet_seed_length_message": "Länge der Seed-Phrase für diese Wallet wählen.",
"add_wallet_seed_length_12": "12 Wörter",
"add_wallet_seed_length_24": "24 Wörter",
"clipboard_bitcoin": "In der Zwischenablage ist eine Bitcoin Adresse. Soll diese für eine Transaktion verwendet werden?",
@ -423,7 +422,6 @@
"details_export_history": "Verlauf als CSV exportieren",
"details_master_fingerprint": "Fingerabdruckkennung",
"details_multisig_type": "Mehrfachsignatur",
"details_no_cancel": "Nein, abbrechnen",
"details_show_xpub": "Wallet xPub zeigen",
"details_show_addresses": "Adressen anzeigen",
"details_title": "Wallet",
@ -494,7 +492,9 @@
"identity_pubkey": "Pubkey-Identität",
"xpub_title": "Wallet xPub",
"manage_wallets_search_placeholder": "Suche in Wallets, Notizen",
"more_info": "Mehr Infos"
"more_info": "Mehr Infos",
"details_delete_wallet_error_message": "Bei der Bestätigung, ob diese Wallet aus den Benachrichtigungen entfernt wurde, trat ein Fehler auf möglicherweise wegen Netzwerkproblemen oder schlechter Verbindung. Falls du fortfährst, erhältst du möglicherweise weiterhin Benachrichtigungen für Transaktionen dieser Wallet, auch nachdem sie gelöscht wurde.",
"details_delete_anyway": "Trotzdem löschen"
},
"total_balance_view": {
"display_in_bitcoin": "In bitcoin anzeigen",
@ -670,7 +670,7 @@
"notification_tx_unconfirmed": "Benachrichtigungstransaktion noch unbestätigt. Bitte warten",
"failed_create_notif_tx": "On-Chain Transaktion konnte nicht in erstellt werden",
"onchain_tx_needed": "On-Chain Transaktion benötigt.",
"notif_tx_sent" : "Benachrichtigungstransaktion ist gesendet. Auf Bestätigung warten.",
"notif_tx_sent": "Benachrichtigungstransaktion ist gesendet. Auf Bestätigung warten.",
"notif_tx": "Benachrichtigungstransaktion",
"not_found": "Zahlungscode nicht gefunden"
}

View file

@ -11,7 +11,6 @@
"storage_is_encrypted": "Το αρχείο σου είναι κρυπτογραφημένο. Χρειάζεται ένας κωδικός για να αποκρυπτογραφηθεί.",
"yes": "Ναι",
"no": "Όχι",
"save": "Αποθήκευση",
"success": "Επιτυχία",
"close": "Κλείσιμο",
"refresh": "Ανανέωση",
@ -168,11 +167,8 @@
"use_ssl": "Χρήση SSL",
"electrum_settings_server": "Διακομιστής Electrum",
"electrum_status": "Κατάσταση",
"electrum_clear_alert_title": "Καθαρισμός ιστορικού;",
"electrum_clear_alert_cancel": "Ακύρωση",
"electrum_clear_alert_ok": "Εντάξει",
"electrum_reset": "Επαναφορά προεπιλογής",
"electrum_unable_to_connect": "Αδυναμία σύνδεσης στον {server}.",
"electrum_reset": "Επαναφορά προεπιλογής",
"encrypt_decrypt": "Αποκρυπτογράφηση αποθηκευτικού χώρου",
"encrypt_title": "Ασφάλεια",
"encrypt_use": "Χρήση {type}",
@ -243,7 +239,6 @@
"details_delete": "Διαγραφή",
"details_delete_wallet": "Διαγραφή πορτοφολιού",
"details_export_backup": "Εξήγαγε / δημιούργησε αντίγραφο ασφαλείας",
"details_no_cancel": "Όχι, ακύρωσε",
"details_show_xpub": "Προβολή XPUB του πορτοφολιού",
"details_show_addresses": "Εμφάνιση διευθύνσεων",
"details_title": "Πορτοφόλι",

View file

@ -14,7 +14,7 @@
"storage_is_encrypted": "Your storage is encrypted. Password is required to decrypt it.",
"yes": "Yes",
"no": "No",
"save": "Save",
"save": "Save...",
"seed": "Seed",
"success": "Success",
"wallet_key": "Wallet key",
@ -136,6 +136,9 @@
"details_add_recc_rem_all_alert_description": "Are you sure you want to remove all recipients?",
"details_add_rec_rem_all": "Remove All Recipients",
"details_recipients_title": "Recipients",
"details_recipient_title": "Recipient #{number} of #{total}",
"please_complete_recipient_title": "Incomplete Recipient",
"please_complete_recipient_details": "Please complete the details of recipient #{number} before adding a new recipient.",
"details_address": "Address",
"details_address_field_is_not_valid": "The address is not valid.",
"details_adv_fee_bump": "Allow Fee Bump",
@ -182,6 +185,7 @@
"input_total": "Total:",
"permission_camera_message": "We need your permission to use your camera.",
"psbt_sign": "Sign a transaction",
"invalid_psbt": "Invalid PSBT provided.",
"open_settings": "Open Settings",
"permission_storage_denied_message": "BlueWallet is unable to save this file. Please open your device settings and enable Storage Permission.",
"permission_storage_title": "Storage Access Permission",
@ -239,6 +243,7 @@
"electrum_connected": "Connected",
"electrum_connected_not": "Not Connected",
"electrum_error_connect": "Cannot connect to the provided Electrum server",
"electrum_error_connect_tor": "Cannot connect to the provided Electrum server. Please make sure Orbot app is connected and try again.",
"lndhub_uri": "E.g., {example}",
"electrum_host": "E.g., {example}",
"electrum_offline_mode": "Offline Mode",
@ -254,8 +259,9 @@
"electrum_preferred_server_description": "Enter the server you want your wallet to use for all Bitcoin activities. Once set, your wallet will exclusively use this server to check balances, send transactions, and fetch network data. Ensure you trust this server before setting it.",
"electrum_unable_to_connect": "Unable to connect to {server}.",
"electrum_history": "History",
"electrum_reset_to_default": "This will let BlueWallet randomly choose a server from the suggested list and history. Your server history will remain unchanged.",
"electrum_reset_to_default": "This will let BlueWallet randomly choose a server from the server list.",
"electrum_reset": "Reset to default",
"electrum_reset_to_default_and_clear_history": "Reset to default and clear history",
"encrypt_decrypt": "Decrypt Storage",
"encrypt_decrypt_q": "Are you sure you want to decrypt your storage? This will allow your wallets to be accessed without a password.",
"encrypt_enc_and_pass": "Encrypted and Password Protected",
@ -284,6 +290,7 @@
"language_isRTL": "Restarting BlueWallet is required for the language orientation to take effect.",
"license": "License",
"lightning_error_lndhub_uri": "Invalid LNDhub URI",
"lightning_error_lndhub_uri_tor": "Invalid LNDhub URI. Please make sure Orbot app is connected and try again.",
"lightning_saved": "Your changes have been saved successfully.",
"lightning_settings": "Lightning Settings",
"lightning_settings_explain": "To connect to your own LND node, please install LNDhub and put its URL here in settings. Please note that only wallets created after saving changes will connect to the specified LNDhub.",
@ -402,7 +409,6 @@
"add_wallet_name": "Name",
"add_wallet_type": "Type",
"add_wallet_seed_length": "Seed Length",
"add_wallet_seed_length_message": "Seed phrase length",
"add_wallet_seed_length_12": "12 words",
"add_wallet_seed_length_24": "24 words",
"clipboard_bitcoin": "You have a Bitcoin address on your clipboard. Would you like to use it for a transaction?",
@ -422,7 +428,6 @@
"details_export_history": "Export History to CSV",
"details_master_fingerprint": "Master Fingerprint",
"details_multisig_type": "multisig",
"details_no_cancel": "No, cancel",
"details_show_xpub": "Show Wallet XPUB",
"details_show_addresses": "Show addresses",
"details_title": "Wallet",
@ -493,7 +498,9 @@
"identity_pubkey": "Identity Pubkey",
"xpub_title": "Wallet XPUB",
"manage_wallets_search_placeholder": "Search wallets, memos",
"more_info": "More Info"
"more_info": "More Info",
"details_delete_wallet_error_message": "There was an issue confirming if this wallet was removed from notifications—this could be due to a network issue or poor connection. If you continue, you might still receive notifications for transactions related to this wallet, even after it is deleted.",
"details_delete_anyway": "Delete anyway"
},
"total_balance_view": {
"display_in_bitcoin": "Display in Bitcoin",
@ -508,6 +515,10 @@
"default_label": "Multisig Vault",
"multisig_vault_explain": "Best security for large amounts",
"provide_signature": "Provide signature",
"provide_signature_details": "Use your device and wallet where the key resides to sign this transaction",
"provide_signature_details_bluewallet": "In BlueWallet, go to the Send screen menu and select ",
"provide_signature_next_steps": "Scan or Import Signed Transaction",
"provide_signature_next_steps_details": "Once your wallet has successfully signed the transaction, scan the provided QR code or import the accompanying file, and then review all the transaction details before broadcasting it.",
"vault_key": "Vault Key {number}",
"required_keys_out_of_total": "Required keys out of the total",
"fee": "Fee: {number}",

View file

@ -12,7 +12,6 @@
"storage_is_encrypted": "Tu almacenamiento está cifrado. Se requiere la contraseña para descifrarlo.",
"yes": "Sí",
"no": "No",
"save": "Guardar",
"seed": "Semilla",
"success": "Completado",
"wallet_key": "Llave de la cartera"
@ -201,13 +200,8 @@
"set_electrum_server_as_default": "¿Establecer {server} como servidor Electrum por defecto?",
"electrum_settings_server": "Servidor Electrum",
"electrum_status": "Estado",
"electrum_clear_alert_title": "¿Limpiar historial?",
"electrum_clear_alert_message": "¿Quieres eliminar el historial de los servidores de Electrum?",
"electrum_clear_alert_cancel": "Cancelar",
"electrum_clear_alert_ok": "Ok",
"electrum_reset": "Restablecer valores predeterminados",
"electrum_unable_to_connect": "Imposible conectar a {server}. ",
"electrum_reset_to_default": "¿Estás seguro de querer reiniciar sus ajustes de Electrum por defecto? ",
"electrum_reset": "Restablecer valores predeterminados",
"encrypt_decrypt": "Desencriptar almacenamiento",
"encrypt_decrypt_q": "¿Seguro que quieres desencriptar tu almacenamiento? Al hacerlo, se podrá acceder a tus carteras sin contraseña.",
"encrypt_enc_and_pass": "Encriptado y protegido mediante contraseña",
@ -327,7 +321,6 @@
"details_export_history": "Exportar el historial a CSV",
"details_master_fingerprint": "Huella dactilar maestra",
"details_multisig_type": "multifirma",
"details_no_cancel": "No, cancelar",
"details_show_xpub": "Mostrar el XPUB de la cartera",
"details_show_addresses": "Mostrar dirección",
"details_title": "Cartera",

View file

@ -10,7 +10,6 @@
"never": "Nunca",
"of": "{number} de {total}",
"ok": "OK",
"customize": "Personalizar",
"enter_url": "Introducir URL",
"storage_is_encrypted": "Tu almacenamiento está encriptado. Se requiere contraseña para descifrarlo.",
"yes": "Sí",
@ -137,6 +136,9 @@
"details_add_recc_rem_all_alert_description": "¿Estás seguro de que quieres eliminar todos los destinatarios?",
"details_add_rec_rem_all": "Eliminar todos los destinatarios",
"details_recipients_title": "Destinatarios",
"details_recipient_title": "Destinatario #{number} de #{total}",
"please_complete_recipient_title": "Destinatario incompleto",
"please_complete_recipient_details": "Completa los detalles del destinatario #{number} antes de agregar un nuevo destinatario.",
"details_address": "Dirección",
"details_address_field_is_not_valid": "La dirección no es válida.",
"details_adv_fee_bump": "Permitir aumento de tarifas",
@ -183,6 +185,7 @@
"input_total": "Total:",
"permission_camera_message": "Necesitamos tu permiso para usar tu cámara",
"psbt_sign": "Firmar una transacción",
"invalid_psbt": "PSBT proporcionado no válido.",
"open_settings": "Abrir configuraciones",
"permission_storage_denied_message": "BlueWallet no puede guardar este archivo. Por favor, abre la configuración de tu dispositivo y activa el permiso de almacenamiento.",
"permission_storage_title": "Permiso de acceso de almacenamiento",
@ -240,6 +243,7 @@
"electrum_connected": "Conectado",
"electrum_connected_not": "No Conectado",
"electrum_error_connect": "No se puede conectar al servidor Electrum proporcionado",
"electrum_error_connect_tor": "No se puede conectar al servidor Electrum proporcionado. Asegúrate de que la aplicación Orbot esté conectada y vuelve a intentarlo.",
"lndhub_uri": "Ej.: {example}",
"electrum_host": "Ej.: {example}",
"electrum_offline_mode": "Modo offline",
@ -255,8 +259,9 @@
"electrum_preferred_server_description": "Introduce el servidor que deseas que tu billetera utilice para todas las actividades de Bitcoin. Una vez configurado, tu billetera utilizará exclusivamente este servidor para comprobar saldos, enviar transacciones y obtener datos de la red. Asegúrate de que confías en este servidor antes de configurarlo.",
"electrum_unable_to_connect": "No se puede conectar al {server}.",
"electrum_history": "Historial",
"electrum_reset_to_default": "Esto permitirá que BlueWallet elija aleatoriamente un servidor de la lista sugerida y del historial. El historial de tu servidor permanecerá sin cambios",
"electrum_reset_to_default": "Esto permitirá que BlueWallet elija aleatoriamente un servidor de la lista de servidores.",
"electrum_reset": "Restablecer a predeterminado",
"electrum_reset_to_default_and_clear_history": "Restablecer valores predeterminados y borrar historial",
"encrypt_decrypt": "Descifrar Almacenamiento",
"encrypt_decrypt_q": "¿Estás seguro de que deseas descifrar tu almacenamiento? Esto permitirá acceder a tus billeteras sin una contraseña.",
"encrypt_enc_and_pass": "Encriptado y protegido con contraseña",
@ -285,6 +290,7 @@
"language_isRTL": "Es necesario reiniciar BlueWallet para que la orientación del idioma surta efecto.",
"license": "Licencia",
"lightning_error_lndhub_uri": "URI de LNDhub no válido",
"lightning_error_lndhub_uri_tor": "La URL de LNDhub no es válida. Asegúrate de que la aplicación Orbot esté conectada y vuelve a intentarlo.",
"lightning_saved": "Tus cambios han sido guardados correctamente.",
"lightning_settings": "Configuración de Lightning",
"lightning_settings_explain": "Para conectarte a tu propio nodo LND, instala LNDhub y coloca su URL aquí en la configuración. Ten en cuenta que solo las billeteras creadas después de guardar los cambios se conectarán al LNDhub especificado.",
@ -403,7 +409,6 @@
"add_wallet_name": "Nombre",
"add_wallet_type": "Tipo",
"add_wallet_seed_length": "Longitud de la semilla",
"add_wallet_seed_length_message": "Elige la longitud de la frase semilla que deseas utilizar para esta billetera.",
"add_wallet_seed_length_12": "12 palabras",
"add_wallet_seed_length_24": "24 palabras",
"clipboard_bitcoin": "Tienes una dirección de Bitcoin en tu portapapeles. ¿te gustaría usarlo para una transacción?",
@ -423,7 +428,6 @@
"details_export_history": "Exportar historial a CSV",
"details_master_fingerprint": "Huella Digital Maestra",
"details_multisig_type": "multifirma",
"details_no_cancel": "No, cancelar",
"details_show_xpub": "Mostrar el XPUB de la Billetera",
"details_show_addresses": "Mostrar direcciones",
"details_title": "Billetera",
@ -494,7 +498,9 @@
"identity_pubkey": "Identidad Pubkey",
"xpub_title": "XPUB de la billetera",
"manage_wallets_search_placeholder": "Buscar billeteras, notas",
"more_info": "Más información"
"more_info": "Más información",
"details_delete_wallet_error_message": "Hubo un problema al confirmar si esta billetera se eliminó de las notificaciones, lo que podría deberse a un problema de red o a una mala conexión. Si continúas, es posible que aún recibas notificaciones de transacciones relacionadas con esta billetera, incluso después de que se elimine.",
"details_delete_anyway": "Borrar de todos modos"
},
"total_balance_view": {
"display_in_bitcoin": "Mostrar en Bitcoin",
@ -509,6 +515,10 @@
"default_label": "Bóveda Multifirma",
"multisig_vault_explain": "La mejor seguridad para grandes cantidades",
"provide_signature": "Proporcionar firma",
"provide_signature_details": "Usa tu dispositivo y billetera donde reside la llave para firmar esta transacción",
"provide_signature_details_bluewallet": "En BlueWallet, ve al menú de la pantalla Enviar y selecciona",
"provide_signature_next_steps": "Escanea o importa la transacción firmada",
"provide_signature_next_steps_details": "Una vez que tu billetera haya firmado con éxito la transacción, escanea el código QR proporcionado o importa el archivo que lo acompaña, y luego revisa todos los detalles de la transacción antes de transmitirla.",
"vault_key": "Clave de la Bóveda {number}",
"required_keys_out_of_total": "Llaves requeridas del total",
"fee": "Tarifa: {number}",
@ -670,7 +680,7 @@
"notification_tx_unconfirmed": "La transacción de notificación aún no está confirmada, espera",
"failed_create_notif_tx": "No se pudo crear una transacción en cadena",
"onchain_tx_needed": "Se necesita transacción en cadena",
"notif_tx_sent" : "Transacción de notificación enviada. Espera a que se confirme",
"notif_tx_sent": "Transacción de notificación enviada. Espera a que se confirme",
"notif_tx": "Transacción de notificación",
"not_found": "Código de pago no encontrado"
}

View file

@ -11,7 +11,6 @@
"storage_is_encrypted": "Sinu hoidla on krüpteeritud. Dekrüpteerimiseks on vajalik sisestada parool.",
"yes": "Jah",
"no": "Ei",
"save": "Salvesta",
"seed": "Seeme",
"success": "Toiming õnnestus",
"wallet_key": "Rahakoti võti"
@ -41,7 +40,6 @@
"create_to": "Sihtkoht"
},
"settings": {
"electrum_clear_alert_cancel": "Katkesta",
"save": "Salvesta"
},
"wallets": {

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