Compare commits

...

752 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
GLaDOS
ae41b9bd0c
Merge pull request #7528 from BlueWallet/entropymenu
REF: Entropy menu as Action instead of alert
2025-01-24 19:19:28 +00:00
Marcos Rodriguez Velez
f6a6d7c41e ADD: Test electrum connection prior to saving 2025-01-24 00:22:20 -04:00
GLaDOS
85ee40b39a
Merge pull request #7525 from BlueWallet/setp
FIX: onBarScanned setparam was not undefined
2025-01-23 20:56:56 +00:00
GLaDOS
2810e2e0a6
Merge pull request #7517 from BlueWallet/pyg
ADD: PYG fiat
2025-01-23 13:36:54 +00:00
Marcos Rodriguez Velez
fcc3bc81ed REF: Reuse existing id 2025-01-23 09:27:41 -04:00
Marcos Rodriguez Velez
cbd05c4408 REF: Entropy menu as Action instead of alert
Android alerts only allow max 3 buttons. This alert requires 4.
2025-01-23 09:19:33 -04:00
GLaDOS
7eb1828150
Merge pull request #7519 from BlueWallet/ent
REF: ProvideEntropy to use routeparams instead of a function
2025-01-23 06:46:45 +00:00
Marcos Rodriguez Velez
75a9bf6f37 FIX: onBarScanned setparam was not undefined 2025-01-23 01:57:42 -04:00
GLaDOS
4b890b2000
Merge pull request #7521 from BlueWallet/renovate/react-native-permissions-5.x
Update dependency react-native-permissions to v5.2.4
2025-01-23 04:47:36 +00:00
GLaDOS
1a4b3d82fc
Merge pull request #7520 from BlueWallet/translations_loc-en-json--master_es_419
Updates for file loc/en.json in es_419
2025-01-23 04:39:27 +00:00
renovate[bot]
c7c4988cd8
Update dependency react-native-permissions to v5.2.4 2025-01-23 03:53:01 +00:00
transifex-integration[bot]
8f34249be5
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2025-01-22 15:51:38 +00:00
Marcos Rodriguez Velez
bc82dc7905 REF: ProvideEntropy to use routeparams instead of a function 2025-01-21 09:35:48 -04:00
Nicholas A. Thompson
5f82d1dc6d
Fix typo in payment codes description (#7515) 2025-01-20 09:10:40 -04:00
Marcos Rodriguez Velez
754a279a76 ADD: PYG fiat 2025-01-19 23:43:56 -04:00
Marcos Rodriguez Velez
cf16417e00 FIX: Allow text to be selectable 2025-01-19 23:42:15 -04:00
GLaDOS
7b3eb806bb
Merge pull request #7508 from BlueWallet/patn
FIX: Error trying to import wallets derived from the same seed in dif…
2025-01-19 22:09:39 +00:00
Marcos Rodriguez Velez
c43103cb71 wip 2025-01-19 15:16:17 -04:00
Marcos Rodriguez Velez
115ac98172 Merge branch 'master' into patn 2025-01-19 15:13:12 -04:00
Marcos Rodriguez Velez
40c5cc7295 REF: MultipleStepsListItem to TSX 2025-01-19 15:12:35 -04:00
GLaDOS
a5fb1bf6f5
Merge pull request #7510 from BlueWallet/translations_loc-en-json--master_fr_FR
Updates for file loc/en.json in fr_FR
2025-01-18 15:48:48 +00:00
GLaDOS
786a06b2ee
Merge pull request #7512 from BlueWallet/translations_loc-en-json--master_de_DE
Updates for file loc/en.json in de_DE
2025-01-18 15:12:25 +00:00
transifex-integration[bot]
27a45ea857
Translate loc/en.json in de_DE
100% reviewed source file: 'loc/en.json'
on 'de_DE'.
2025-01-18 12:40:16 +00:00
transifex-integration[bot]
f86afd4092
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:32:49 +00:00
transifex-integration[bot]
be5a61e991
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:31:54 +00:00
transifex-integration[bot]
f315b03b0e
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:21:00 +00:00
transifex-integration[bot]
fb1c221635
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:20:16 +00:00
transifex-integration[bot]
a24285e06e
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:18:43 +00:00
transifex-integration[bot]
6b3a181714
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:17:03 +00:00
transifex-integration[bot]
f506140c92
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:16:29 +00:00
transifex-integration[bot]
9799aaecc6
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:12:49 +00:00
transifex-integration[bot]
8d66e515b7
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:08:11 +00:00
transifex-integration[bot]
74a6033e65
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:07:20 +00:00
transifex-integration[bot]
b6806ac412
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:06:56 +00:00
transifex-integration[bot]
5b367c5ffb
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:06:45 +00:00
transifex-integration[bot]
2697a45ff2
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:06:37 +00:00
transifex-integration[bot]
5c7b8ad3cc
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:06:21 +00:00
transifex-integration[bot]
c5cfa9d467
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:05:37 +00:00
transifex-integration[bot]
df0e82483e
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:05:26 +00:00
transifex-integration[bot]
8f54885991
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:05:11 +00:00
transifex-integration[bot]
238a5c2d09
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:04:39 +00:00
transifex-integration[bot]
999123e497
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-17 17:04:29 +00:00
Marcos Rodriguez Velez
ea421715d6 Update useExtendedNavigation.ts 2025-01-17 10:09:34 -04:00
Marcos Rodriguez Velez
845ac41928 Update ImportWalletDiscovery.tsx 2025-01-17 10:02:43 -04:00
Marcos Rodriguez Velez
38e26ccbd6 FIX: Error trying to import wallets derived from the same seed in different accounts after updating 2025-01-16 21:50:14 -04:00
GLaDOS
a8c4fdd20c
Merge pull request #7506 from BlueWallet/renovate/lottie-react-native-7.x
Update dependency lottie-react-native to v7.2.2
2025-01-16 22:02:26 +00:00
Marcos Rodriguez Velez
6d78b5a141 OPS: Version bump 2025-01-16 16:13:52 -04:00
renovate[bot]
ef21fcfde8
Update dependency lottie-react-native to v7.2.2 2025-01-16 16:28:38 +00:00
GLaDOS
6efe21f6b0
Merge pull request #7501 from BlueWallet/vault
REF: Use FP for consistent multisig get ID
2025-01-15 18:49:33 +00:00
GLaDOS
7e5f5f50c6
Merge pull request #7502 from BlueWallet/mock
DEL: Mock entry no longer needed
2025-01-15 18:49:30 +00:00
Marcos Rodriguez Velez
aff43a4f1e Merge branch 'master' into mock 2025-01-15 13:38:01 -04:00
Marcos Rodriguez Velez
0de78149ab DEL: Mock entry no longer needed 2025-01-15 13:37:52 -04:00
Marcos Rodriguez VĂ©lez
6cdd627272
FIX: Save button was to close to the safearea (#7498) 2025-01-15 13:36:58 -04:00
Marcos Rodriguez Velez
647ddcd28b REF: Use FP for consistent multisig get ID 2025-01-15 13:29:31 -04:00
GLaDOS
1d3c62a5ab
Merge pull request #7495 from BlueWallet/translations_loc-en-json--master_fr_FR
Updates for file loc/en.json in fr_FR
2025-01-15 03:44:21 +00:00
GLaDOS
906ca1a55d
Merge pull request #7499 from BlueWallet/renovate/lodev09-react-native-true-sheet-digest
Update @lodev09/react-native-true-sheet digest to c6fee89
2025-01-15 03:44:18 +00:00
GLaDOS
e740172d57
Merge pull request #7500 from BlueWallet/renovate/react-native-clipboard-clipboard-1.x
Update dependency @react-native-clipboard/clipboard to v1.16.1
2025-01-15 00:56:55 +00:00
renovate[bot]
018796ac3f
Update dependency @react-native-clipboard/clipboard to v1.16.1 2025-01-15 00:01:44 +00:00
renovate[bot]
5bb5430232
Update @lodev09/react-native-true-sheet digest to c6fee89 2025-01-14 23:59:43 +00:00
GLaDOS
190b0f2435
Merge pull request #7487 from BlueWallet/navc
FIX: Dont show alert until navigation is ready
2025-01-14 23:49:44 +00:00
GLaDOS
d6586cbfb7
Merge pull request #7497 from BlueWallet/c
FIX; Camera safe area UI background was white.
2025-01-14 23:49:37 +00:00
Marcos Rodriguez Velez
66fe1dc7ae Update Alert.ts 2025-01-14 18:53:50 -04:00
Marcos Rodriguez Velez
a2db75539c FIX; Camera safe area UI background was white. 2025-01-14 18:52:17 -04:00
Overtorment
756f37eeb4
Merge branch 'master' into navc 2025-01-14 17:01:11 +00:00
transifex-integration[bot]
e514af1aa6
Translate loc/en.json in fr_FR
100% reviewed source file: 'loc/en.json'
on 'fr_FR'.
2025-01-14 14:48:22 +00:00
GLaDOS
ca5eff1730
Merge pull request #7493 from BlueWallet/exc
FIX: Excessive number of pending callbacks error
2025-01-13 22:47:16 +00:00
Marcos Rodriguez VĂ©lez
10bf267c36
Merge branch 'master' into exc 2025-01-13 15:41:49 -04:00
Marcos Rodriguez VĂ©lez
7643ce6821
Update ImportWalletDiscovery.tsx 2025-01-13 15:41:25 -04:00
GLaDOS
f9dce1b120
Merge pull request #7492 from BlueWallet/renovate/react-native-reanimated-3.x
Update dependency react-native-reanimated to v3.16.7
2025-01-13 17:50:54 +00:00
GLaDOS
0b9df766df
Merge pull request #7494 from BlueWallet/renovate/react-native-permissions-5.x
Update dependency react-native-permissions to v5.2.3
2025-01-13 17:11:21 +00:00
renovate[bot]
b1aa371631
Update dependency react-native-permissions to v5.2.3 2025-01-13 14:25:07 +00:00
Marcos Rodriguez VĂ©lez
7e4447f5cf
FIX: Camera was not scanning QR (#7486) 2025-01-13 14:22:58 +00:00
Marcos Rodriguez VĂ©lez
1c8e9e88cf
Merge branch 'master' into renovate/react-native-reanimated-3.x 2025-01-13 09:53:23 -04:00
GLaDOS
4be89fbc9b
Merge pull request #7491 from BlueWallet/translations_loc-en-json--master_pl
Updates for file loc/en.json in pl
2025-01-13 13:18:41 +00:00
Marcos Rodriguez VĂ©lez
49ab9e635c
Merge branch 'master' into renovate/react-native-reanimated-3.x 2025-01-13 09:10:57 -04:00
Marcos Rodriguez Velez
5cba7cc2c7 FIX: Excessive number of pending callbacks error 2025-01-13 08:59:15 -04:00
Marcos Rodriguez Velez
6193e9bac0 Revert "wio"
This reverts commit bba96e5308.
2025-01-13 08:51:40 -04:00
renovate[bot]
0a0dd366bd
Update dependency react-native-reanimated to v3.16.7 2025-01-13 10:38:05 +00:00
Marcos Rodriguez Velez
bba96e5308 wio 2025-01-12 15:55:26 -04:00
transifex-integration[bot]
16a8e7ae61
Translate loc/en.json in pl
100% reviewed source file: 'loc/en.json'
on 'pl'.
2025-01-12 13:37:41 +00:00
GLaDOS
70c69eb7ca
Merge pull request #7489 from BlueWallet/renovate/react-native-gesture-handler-2.x
Update dependency react-native-gesture-handler to v2.22.0
2025-01-11 18:26:53 +00:00
renovate[bot]
32e2fc6ca3
Update dependency react-native-gesture-handler to v2.22.0 2025-01-10 15:58:02 +00:00
GLaDOS
efc768f642
Merge pull request #7485 from BlueWallet/renovate/react-native-svg-15.x
Update dependency react-native-svg to v15.11.1
2025-01-10 06:46:11 +00:00
Marcos Rodriguez Velez
fc27e52bb8 Update Alert.ts 2025-01-10 00:05:15 -04:00
Marcos Rodriguez Velez
9a0d76cc9c FIX: Dont show alert until navigation is ready 2025-01-09 21:44:13 -04:00
GLaDOS
c59f4c1daa
Merge pull request #7484 from BlueWallet/renovate/react-native-localize-3.x
Update dependency react-native-localize to v3.4.1
2025-01-09 22:38:44 +00:00
GLaDOS
3b149c7cf0
Merge pull request #7482 from BlueWallet/translations_loc-en-json--master_de_DE
Updates for file loc/en.json in de_DE
2025-01-09 22:29:09 +00:00
renovate[bot]
52441c1d63
Update dependency react-native-svg to v15.11.1 2025-01-09 21:45:28 +00:00
renovate[bot]
adbe625905
Update dependency react-native-localize to v3.4.1 2025-01-09 16:12:12 +00:00
transifex-integration[bot]
26a735ed62
Translate loc/en.json in de_DE
100% reviewed source file: 'loc/en.json'
on 'de_DE'.
2025-01-09 12:45:32 +00:00
Matheus Marrane
405dfee7dc FIX: fix what test expects to see after change 2025-01-08 20:53:51 +00:00
Matheus Marrane
94cc243d20 update comment 2025-01-08 20:53:51 +00:00
Matheus Marrane
fdfb55d3e2 fix: ensure bech32 addresses use uppercase as per BIP 0173
- Adjusted QR code generation to use uppercase for bech32 addresses, aligning with BIP 0173 specifications for optimal QR encoding efficiency.
2025-01-08 20:53:51 +00:00
GLaDOS
421b30a130
Merge pull request #7425 from BlueWallet/renovate/gradle-8.x
Update dependency gradle to v8.12
2025-01-08 18:32:36 +00:00
GLaDOS
5045b8566f
Merge pull request #7479 from BlueWallet/translations_loc-en-json--master_es_419
Updates for file loc/en.json in es_419
2025-01-08 18:32:24 +00:00
GLaDOS
a92837dc35
Merge pull request #7455 from BlueWallet/renovate/rubyzip-2.x
Update dependency rubyzip to v2.4.1
2025-01-08 13:15:08 +00:00
Marcos Rodriguez VĂ©lez
bd9340c756
Merge branch 'master' into renovate/gradle-8.x 2025-01-08 09:13:37 -04:00
transifex-integration[bot]
d247f3eaff
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2025-01-08 12:49:27 +00:00
GLaDOS
65c8798dba
Merge pull request #7477 from BlueWallet/renovate/react-native-clipboard-clipboard-1.x
Update dependency @react-native-clipboard/clipboard to v1.16.0
2025-01-08 12:46:37 +00:00
GLaDOS
4e050b33cf
Merge pull request #7478 from BlueWallet/renovate/lottie-react-native-7.x
Update dependency lottie-react-native to v7.2.1
2025-01-08 12:46:34 +00:00
renovate[bot]
5ec064eb6f
Update dependency lottie-react-native to v7.2.1 2025-01-08 10:48:51 +00:00
renovate[bot]
4169c8e499
Update dependency @react-native-clipboard/clipboard to v1.16.0 2025-01-08 10:40:02 +00:00
GLaDOS
3532840b5a
Merge pull request #7449 from BlueWallet/electrumpref
REF: Make Server history menu less confusing
2025-01-08 10:35:23 +00:00
GLaDOS
02340a3e9f
Merge pull request #7475 from BlueWallet/drag
FIX: Allow manage list to be scrollable
2025-01-08 10:35:08 +00:00
GLaDOS
da7885febb
Merge pull request #7476 from BlueWallet/cameraic
REF: Use Icons from icon packcage
2025-01-08 10:35:04 +00:00
Marcos Rodriguez Velez
f1bc844977 Update ElectrumSettings.tsx 2025-01-08 00:53:09 -04:00
Marcos Rodriguez Velez
2546f9015a Merge branch 'electrumpref' of https://github.com/BlueWallet/BlueWallet into electrumpref 2025-01-08 00:51:28 -04:00
Marcos Rodriguez Velez
342f461bdf Merge branch 'master' into electrumpref 2025-01-08 00:51:15 -04:00
Marcos Rodriguez VĂ©lez
3932f4f90d
Update screen/settings/ElectrumSettings.tsx
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-01-08 00:50:44 -04:00
Marcos Rodriguez VĂ©lez
57b74f10e2
Update screen/settings/ElectrumSettings.tsx
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-01-08 00:50:31 -04:00
Marcos Rodriguez Velez
1df15e3a00 REF: Use Icons from icon packcage 2025-01-08 00:12:41 -04:00
Marcos Rodriguez Velez
983034c788 Update ManageWalletsListItem.tsx 2025-01-07 23:49:45 -04:00
Marcos Rodriguez Velez
4e480b20c4 Update ManageWalletsListItem.tsx 2025-01-07 23:48:40 -04:00
Marcos Rodriguez Velez
010b99a11b Revert "Update ManageWalletsListItem.tsx"
This reverts commit 555ba1d9ea.
2025-01-07 23:47:15 -04:00
Marcos Rodriguez Velez
555ba1d9ea Update ManageWalletsListItem.tsx 2025-01-07 23:45:59 -04:00
Marcos Rodriguez Velez
30ab6d7883 Update ManageWalletsListItem.tsx 2025-01-07 23:23:23 -04:00
Marcos Rodriguez Velez
b500d6ab58 wip 2025-01-07 23:16:53 -04:00
Marcos Rodriguez Velez
4633d4b4ea Update ManageWallets.tsx 2025-01-07 23:10:49 -04:00
Marcos Rodriguez Velez
d0fdb6b28d Merge branch 'master' into drag 2025-01-07 23:09:06 -04:00
Marcos Rodriguez Velez
56f3ac22c2 FIX: Allow manage list to be scrollable 2025-01-07 22:20:09 -04:00
GLaDOS
ca24a1eaa8
Merge pull request #7459 from BlueWallet/marcosrdz-patch-3
Update build-release-apk.yml
2025-01-07 22:56:22 +00:00
GLaDOS
79088297b7
Merge pull request #7474 from BlueWallet/reord
REF: As per docs, modals should  be last
2025-01-07 22:48:06 +00:00
Marcos Rodriguez Velez
3a6303ebd4 REF: As per docs, modals should be last 2025-01-07 15:31:14 -04:00
Marcos Rodriguez VĂ©lez
f5854f48ee
Update Fastfile 2025-01-07 15:26:39 -04:00
Marcos Rodriguez VĂ©lez
e30c0b17ea
Merge branch 'master' into marcosrdz-patch-3 2025-01-07 15:15:18 -04:00
Marcos Rodriguez Velez
1154a6a523 Update Fastfile 2025-01-07 15:14:51 -04:00
Marcos Rodriguez Velez
8ba1f3d1d4 wip 2025-01-07 14:53:38 -04:00
Marcos Rodriguez Velez
bbf3324b57 Update ElectrumSettings.tsx 2025-01-07 14:52:34 -04:00
Marcos Rodriguez Velez
2dc3efd391 Merge branch 'master' into electrumpref 2025-01-07 14:48:13 -04:00
GLaDOS
0ee3da9dc0
Merge pull request #7472 from BlueWallet/cameraki
REF: Upgrade Camera kit
2025-01-07 17:32:00 +00:00
GLaDOS
5e1b8b1c4e
Merge pull request #7465 from BlueWallet/mana
REF: Manage Wallet to a different better package
2025-01-07 17:21:51 +00:00
GLaDOS
40d8b86859
Merge pull request #7466 from BlueWallet/inputac
ADD: Keyboard accessory on vault modal
2025-01-07 17:21:48 +00:00
GLaDOS
0bc7617148
Merge pull request #7467 from BlueWallet/marcosrdz-patch-4
Update Info.plist
2025-01-07 17:21:44 +00:00
GLaDOS
584f39f0aa
Merge pull request #7452 from BlueWallet/coins
FIX: Coin Selected bar was visible without coins selected
2025-01-07 16:48:13 +00:00
Marcos Rodriguez Velez
714702aac0 Update DetailViewStackParamList.ts 2025-01-07 11:51:11 -04:00
Marcos Rodriguez VĂ©lez
84227e1457
DEL: File 2025-01-07 00:21:37 -04:00
Marcos Rodriguez VĂ©lez
0b096f77d6
Merge branch 'master' into electrumpref 2025-01-06 20:47:01 -04:00
Marcos Rodriguez VĂ©lez
8083899b06
Merge branch 'master' into cameraki 2025-01-06 20:45:15 -04:00
Marcos Rodriguez Velez
008b7d98a8 Update SendDetails.tsx 2025-01-06 20:34:09 -04:00
Marcos Rodriguez Velez
13254b0045 Update ScanQRCode.js 2025-01-06 20:31:36 -04:00
Marcos Rodriguez Velez
fcbc563916 Merge branch 'master' into coins 2025-01-06 20:31:21 -04:00
Marcos Rodriguez Velez
73637a9ff6 Update ScanQRCode.js 2025-01-06 20:24:46 -04:00
Marcos Rodriguez Velez
2ccd73e2f1 Revert "Update SendDetails.tsx"
This reverts commit e88ce6f505.
2025-01-06 20:24:41 -04:00
Marcos Rodriguez Velez
ea44f87490 Update addMultisigStep2.js 2025-01-06 20:11:33 -04:00
Marcos Rodriguez Velez
9133bfbedb Merge branch 'master' into inputac 2025-01-06 20:11:17 -04:00
Marcos Rodriguez Velez
6b326dc70d wip 2025-01-06 20:10:32 -04:00
Marcos Rodriguez Velez
4ca37246ab Merge branch 'master' into mana 2025-01-06 20:08:19 -04:00
Marcos Rodriguez Velez
2a5003e9e7 Update LightningSettings.tsx 2025-01-06 20:07:56 -04:00
Marcos Rodriguez Velez
5265ae8bd0 Update CameraScreen.tsx 2025-01-06 20:07:13 -04:00
Marcos Rodriguez Velez
da5078290d REF: Upgrade. camera kit 2025-01-06 20:05:55 -04:00
GLaDOS
1c01d133e0
Merge pull request #7463 from BlueWallet/structure
FIX: structuredClone isnt available on RN
2025-01-06 19:10:20 +00:00
GLaDOS
1db664545b
Merge pull request #7464 from BlueWallet/icons
ADD: Android menu icons
2025-01-06 19:10:16 +00:00
GLaDOS
534fe22d2d
Merge pull request #7469 from BlueWallet/translations_loc-en-json--master_es_419
Updates for file loc/en.json in es_419
2025-01-06 18:15:18 +00:00
transifex-integration[bot]
6749f501e9
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2025-01-06 13:31:16 +00:00
transifex-integration[bot]
e1e17dddde
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2025-01-06 13:30:24 +00:00
transifex-integration[bot]
c0490804bb
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2025-01-06 13:29:21 +00:00
transifex-integration[bot]
a2b8409710
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2025-01-06 13:27:13 +00:00
transifex-integration[bot]
81140623c3
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2025-01-06 13:25:43 +00:00
transifex-integration[bot]
58591566d3
Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2025-01-06 13:11:34 +00:00
renovate[bot]
8da871a071
Update dependency rubyzip to v2.4.1 2025-01-05 21:11:11 +00:00
Marcos Rodriguez VĂ©lez
a1deef2474
Update Info.plist 2025-01-05 16:32:33 -04:00
Marcos Rodriguez Velez
eb9f210b5b ADD: Keyboard accessory on vault modal 2025-01-05 16:17:13 -04:00
Marcos Rodriguez Velez
8e28fc2395 REF: Manage Wallet to a different better package 2025-01-05 15:42:45 -04:00
Marcos Rodriguez Velez
eb298f8669 ADD: Android menu icons 2025-01-05 14:47:33 -04:00
Marcos Rodriguez Velez
0415b8f9aa Update Podfile.lock 2025-01-05 14:37:27 -04:00
Marcos Rodriguez Velez
d9840b3202 FIX: structuredClone isnt available on RN 2025-01-05 14:33:59 -04:00
GLaDOS
4f4131c7cd
Merge pull request #7460 from BlueWallet/renovate/react-native-permissions-5.x
Update dependency react-native-permissions to v5.2.2
2025-01-05 18:15:55 +00:00
Marcos Rodriguez Velez
43a856e8e1 Update CommonToolTipActions.ts 2025-01-05 13:52:47 -04:00
Marcos Rodriguez Velez
8d07f33051 wip 2025-01-05 13:50:27 -04:00
Marcos Rodriguez Velez
7d60b6fc8c wip 2025-01-05 13:47:26 -04:00
Marcos Rodriguez Velez
9555e5927e Merge branch 'master' into electrumpref 2025-01-05 13:43:10 -04:00
renovate[bot]
8ddfec67b8
Update dependency react-native-permissions to v5.2.2 2025-01-05 16:38:48 +00:00
GLaDOS
99ba338735
Merge pull request #7450 from BlueWallet/qrc
FIX: If theres no valid file then dont attempt to scan for qr
2025-01-05 13:59:28 +00:00
GLaDOS
fd15e83fe8
Merge pull request #7451 from BlueWallet/and
FIX: Price value should autosize
2025-01-05 13:59:25 +00:00
GLaDOS
8f46905895
Merge pull request #7456 from BlueWallet/scanq
FIX: Ask for camera auth
2025-01-05 13:59:21 +00:00
GLaDOS
40a05a7a3a
Merge pull request #7457 from BlueWallet/loadindi
REF: Add loading indicator to Edit Vault row
2025-01-05 13:59:18 +00:00
GLaDOS
5b7254b626
Merge pull request #7458 from BlueWallet/hdk
ADD: HKD fiat
2025-01-05 13:59:16 +00:00
Marcos Rodriguez VĂ©lez
e407c0dcff
Update Fastfile 2025-01-05 01:49:17 -04:00
Marcos Rodriguez VĂ©lez
3e06e029f2
Update build-release-apk.yml 2025-01-05 01:43:35 -04:00
Marcos Rodriguez Velez
e68b6936e6 ADD: HKD fiat 2025-01-04 22:32:26 -04:00
Marcos Rodriguez Velez
8b45f11441 REF: Add loading indicator to Edit Vault row 2025-01-04 19:59:46 -04:00
Marcos Rodriguez Velez
b971433245 FIX: Ask for camera auth 2025-01-04 19:31:08 -04:00
GLaDOS
13bb11b062
Merge pull request #7445 from BlueWallet/marcosrdz-patch-3
Update LightningSettings.tsx
2025-01-03 20:50:18 +00:00
Marcos Rodriguez VĂ©lez
e88ce6f505
Update SendDetails.tsx 2025-01-03 14:49:42 -04:00
GLaDOS
f430ed5087
Merge pull request #7448 from BlueWallet/fix-short-ms-path
FIX: import Casa multisig wallet descriptor (closes #7395)
2025-01-03 17:35:07 +00:00
Marcos Rodriguez Velez
297e8d8e42 REF: Reuse fs class 2025-01-03 13:34:55 -04:00
Marcos Rodriguez Velez
28f893f91f Update SendDetails.tsx 2025-01-03 11:31:15 -04:00
Marcos Rodriguez Velez
93216d0145 FIX: Coin Selected bar was visible without coins selected 2025-01-03 11:29:12 -04:00
Marcos Rodriguez VĂ©lez
8ea5583458
REF: ScanQRCode navigation (#7444) 2025-01-03 06:14:09 -04:00
Marcos Rodriguez VĂ©lez
01df140eef
Update Info.plist 2025-01-02 23:10:10 -04:00
Marcos Rodriguez Velez
61097732cb FIX: Price value should autosize 2025-01-02 22:05:07 -04:00
Marcos Rodriguez Velez
92c8bdc202 FIX: If theres no valid file then dont attempt to scan for qr 2025-01-02 20:58:04 -04:00
Marcos Rodriguez Velez
22acb13463 REF: Make Server history menu less confusing 2025-01-02 20:11:08 -04:00
GLaDOS
cbfc16e29b
Merge pull request #7447 from BlueWallet/translations_loc-en-json--master_de_DE
Updates for file loc/en.json in de_DE
2025-01-02 18:39:10 +00:00
overtorment
17f95d5634 FIX: import Casa multisig wallet descriptor (closes #7395) 2025-01-02 14:55:37 +00:00
transifex-integration[bot]
03d695eb91
Translate loc/en.json in de_DE
100% reviewed source file: 'loc/en.json'
on 'de_DE'.
2025-01-02 13:59:49 +00:00
GLaDOS
f13bb4dc53
Merge pull request #7436 from BlueWallet/electrummenu
ADD: Set preferred server from menu
2025-01-02 13:23:22 +00:00
GLaDOS
636fc21f9c
Merge pull request #7440 from BlueWallet/androidwi
FIX: Android widget fixes. Allow other currencies
2025-01-02 13:09:26 +00:00
GLaDOS
23f6905191
Merge pull request #7438 from BlueWallet/bip47togglr
FIX: Toggle for BIP47 was not visible
2025-01-02 12:39:37 +00:00
GLaDOS
d50de5975a
Merge pull request #7434 from BlueWallet/wo
feat: import xpub as zpub/ypub if it was ever used
2025-01-02 12:20:27 +00:00
Stefan EiĂźler
68262504b5 REF: Remove unused Button prop 2025-01-02 12:16:16 +00:00
Stefan EiĂźler
28d9fa1d53 REF: Implement Suggestions 2025-01-02 12:16:16 +00:00
Stefan EiĂźler
841f49ceb9 Fix linting warnings and adjust code formatting 2025-01-02 12:16:16 +00:00
Stefan EiĂźler
156010dbbd ADD: Display Lightning details in Invoice View 2025-01-02 12:16:16 +00:00
Marcos Rodriguez VĂ©lez
e93259e39e
Update LightningSettings.tsx 2025-01-01 21:07:51 -04:00
Marcos Rodriguez VĂ©lez
c5c2ce8a61
Update LightningSettings.tsx 2025-01-01 20:32:28 -04:00
Marcos Rodriguez VĂ©lez
86662cad0d
Merge branch 'master' into bip47togglr 2024-12-31 09:35:40 -04:00
GLaDOS
cbda4cafab
Merge pull request #7439 from BlueWallet/untiref
REF: Make unit part of address array
2024-12-31 11:25:52 +00:00
Marcos Rodriguez Velez
7cb22e9f50 Revert "Update SendDetails.tsx"
This reverts commit 0964c1843a.
2024-12-30 19:24:54 -04:00
Marcos Rodriguez Velez
c4bca5e1c5 wip 2024-12-30 18:03:15 -04:00
Marcos Rodriguez VĂ©lez
32a5627132
Update bluewallet.spec.js 2024-12-29 17:58:28 -04:00
Marcos Rodriguez Velez
6ac5356683 Update WidgetUpdateWorker.kt 2024-12-29 17:24:44 -04:00
Marcos Rodriguez Velez
9bdfcac63f FIX: Android widget fixes. Allow other currencies 2024-12-29 17:10:06 -04:00
Marcos Rodriguez VĂ©lez
42fce50ffe
Update bluewallet.spec.js 2024-12-29 16:35:44 -04:00
Marcos Rodriguez Velez
0964c1843a Update SendDetails.tsx 2024-12-29 16:30:09 -04:00
Marcos Rodriguez Velez
8a0c7a18ce REF: Make unit part of address array 2024-12-29 16:09:08 -04:00
Marcos Rodriguez Velez
3a92b661b3 FIX: Toggle for BIP47 was not visible 2024-12-29 15:35:10 -04:00
Marcos Rodriguez Velez
6a8b794963 wip 2024-12-29 15:25:51 -04:00
Marcos Rodriguez Velez
7fd8097e42 Update BlueElectrum.ts 2024-12-29 15:23:40 -04:00
Marcos Rodriguez Velez
4094207244 Update bluewallet.spec.js 2024-12-29 15:18:41 -04:00
Marcos Rodriguez VĂ©lez
88b7994700
Update bluewallet.spec.js 2024-12-29 06:33:10 -04:00
Marcos Rodriguez Velez
57ce1f87a1 Update CommonToolTipActions.ts 2024-12-29 05:01:15 -04:00
Marcos Rodriguez Velez
20365b6a16 Merge branch 'master' into electrummenu 2024-12-29 05:00:55 -04:00
Marcos Rodriguez Velez
8634df94f3 Revert "Update bluewallet.spec.js"
This reverts commit abddacac72.
2024-12-29 05:00:01 -04:00
Marcos Rodriguez VĂ©lez
06e401e6da
Merge branch 'master' into electrummenu 2024-12-29 04:58:55 -04:00
Marcos Rodriguez VĂ©lez
abddacac72
Update bluewallet.spec.js 2024-12-29 04:57:42 -04:00
GLaDOS
93883b2d9b
Merge pull request #7435 from BlueWallet/renovate/react-native-menu-menu-digest
Update @react-native-menu/menu digest to 14bab79
2024-12-29 05:43:06 +00:00
GLaDOS
f3c5fb4eed
Merge pull request #7433 from BlueWallet/locsync18
fix: sync language files
2024-12-29 05:34:41 +00:00
Marcos Rodriguez VĂ©lez
b1a71b5d0c
Merge branch 'master' into electrummenu 2024-12-29 01:31:49 -04:00
Marcos Rodriguez VĂ©lez
f67d852f28
Update ElectrumSettings.tsx 2024-12-29 01:29:53 -04:00
Marcos Rodriguez Velez
a76c847a10 ADD: Set preferred server from menu 2024-12-29 01:26:10 -04:00
renovate[bot]
d3aed6fe72
Update @react-native-menu/menu digest to 14bab79 2024-12-29 04:36:28 +00:00
Ivan Vershigora
a14499a6ba
feat: import xpub as zpub/ypub if it was ever used 2024-12-28 17:54:35 +00:00
Ivan Vershigora
26504d90e4
fix: sync language files 2024-12-28 10:50:13 +00:00
Marcos Rodriguez VĂ©lez
06f8ba9248
Revert "Update UnlockWith.tsx" (#7429) 2024-12-24 23:44:25 -04:00
GLaDOS
cc8bedadb3
Merge pull request #7420 from BlueWallet/read
FIX: Readable colors on PriceView
2024-12-23 10:36:00 +00:00
GLaDOS
00e94f1a86
Merge pull request #7421 from BlueWallet/Vault
FIX: Vault UI fixes
2024-12-23 10:35:54 +00:00
GLaDOS
6550e955f6
Merge pull request #7423 from BlueWallet/marcosrdz-patch-3
Update UnlockWith.tsx
2024-12-23 10:35:50 +00:00
GLaDOS
7862e202e7
Merge pull request #7424 from BlueWallet/renovate/react-native-reanimated-3.x
Update dependency react-native-reanimated to v3.16.6
2024-12-20 19:00:16 +00:00
renovate[bot]
6b4f2d6adc
Update dependency gradle to v8.12 2024-12-20 18:08:46 +00:00
renovate[bot]
e97f711e32
Update dependency react-native-reanimated to v3.16.6 2024-12-20 18:01:08 +00:00
Marcos Rodriguez VĂ©lez
06e0c7b4fe
Update UnlockWith.tsx 2024-12-19 18:43:49 -04:00
Marcos Rodriguez Velez
8046955af8 FIX: Vault UI fixes 2024-12-19 09:03:34 -04:00
Marcos Rodriguez Velez
0aaea9ce86 FIX: Readable colors on PriceView 2024-12-18 21:09:10 -04:00
Marcos Rodriguez Velez
1df7e8f580 Update BottomModal.tsx 2024-12-17 20:45:55 -04:00
GLaDOS
89b0883837
Merge pull request #7417 from BlueWallet/fx
FIX: QR recognizer and Cosign PSBT
2024-12-16 21:09:07 +00:00
GLaDOS
2b8846c2f7
Merge pull request #7418 from BlueWallet/delt
REF: Move some Wallet Details options to menu
2024-12-16 20:58:16 +00:00
Marcos Rodriguez VĂ©lez
2a9f8858c3
Update components/AddressInputScanButton.tsx
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-12-16 16:08:44 -04:00
Marcos Rodriguez Velez
ba5e566b90 REF: Move some Wallet Details options to menu 2024-12-16 15:34:34 -04:00
Marcos Rodriguez Velez
79f05ec12a Update Podfile.lock 2024-12-16 15:03:19 -04:00
Marcos Rodriguez Velez
268a2fe6a2 FIX: QR recognizer and Cosign PSBT 2024-12-16 14:45:16 -04:00
GLaDOS
1b9812a5b6
Merge pull request #7414 from BlueWallet/multisigfix
FIX: Vault UI fixes
2024-12-16 17:41:05 +00:00
GLaDOS
fb85f3e252
Merge pull request #7415 from BlueWallet/fsref
FIX: FIle picker was double closing modal
2024-12-16 17:20:55 +00:00
GLaDOS
e56ab8b498
Merge pull request #7416 from BlueWallet/fix-ln-incoming-pending-icon
FIX: unpaid ln invoices were rendered as paid
2024-12-16 12:31:39 +00:00
overtorment
750308725e FIX: unpaid ln invoices were rendered as paid 2024-12-16 11:16:44 +00:00
Marcos Rodriguez Velez
1a5d670b72 Merge branch 'fsref' into multisigfix 2024-12-15 01:12:56 -04:00
Marcos Rodriguez Velez
9ce4188a72 FIX: FIle picker was double closing modal 2024-12-15 01:12:31 -04:00
Marcos Rodriguez Velez
fed5da66b9 wip 2024-12-14 23:43:50 -04:00
Marcos Rodriguez Velez
b08fcf390e wip 2024-12-14 23:38:08 -04:00
Marcos Rodriguez Velez
541d6aa206 FIX: Vault UI modals were broken 2024-12-14 23:18:27 -04:00
GLaDOS
5e2e0b58c2
Merge pull request #7411 from BlueWallet/mod
FIX: Cmponent had changes internally that required update
2024-12-14 20:41:15 +00:00
GLaDOS
8de33f9b86
Merge pull request #7407 from BlueWallet/marketw
FIX: Widgets do not work any longer on iOS and macOS #7380
2024-12-14 20:08:23 +00:00
Marcos Rodriguez Velez
b66710b5b9 Update notifications.js 2024-12-14 20:02:54 +00:00
GLaDOS
cefe725e14
Merge pull request #7406 from BlueWallet/translations_loc-en-json--master_es_419
Updates for file loc/en.json in es_419
2024-12-14 19:40:48 +00:00
Marcos Rodriguez Velez
0549b86330 Update PromptPasswordConfirmationModal.tsx 2024-12-14 15:38:22 -04:00
Marcos Rodriguez Velez
fa4225619f FIX: Cmponent had changes internally that required update 2024-12-14 15:38:19 -04:00
transifex-integration[bot]
5be8d6733d Translate loc/en.json in es_419
100% reviewed source file: 'loc/en.json'
on 'es_419'.
2024-12-14 11:53:19 +00:00
GLaDOS
17f0b80aae
Merge pull request #7410 from BlueWallet/brows
FIX: App was being offered as a default web browser replacement
2024-12-13 20:29:42 +00:00
Marcos Rodriguez Velez
62c5eda2b2 FIX: App was being offered as a default web browser replacement 2024-12-13 15:27:03 -04:00
Marcos Rodriguez Velez
2ee82f8dc9 wip 2024-12-12 23:38:41 -04:00
Marcos Rodriguez Velez
ed8b8e32ae wip 2024-12-12 23:21:17 -04:00
Marcos Rodriguez VĂ©lez
2c552b7963
Update ios/Shared/MarketAPI+Electrum.swift
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-12-12 23:14:20 -04:00
Marcos Rodriguez Velez
cccd4bc688 FIX: Widgets do not work any longer on iOS and macOS #7380 2024-12-12 23:03:05 -04:00
Overtorment
7e4cda4b26
Merge pull request #7405 from BlueWallet/tst-flaky-integration
TST: improve flaky integration tests
2024-12-12 20:28:28 +00:00
overtorment
d954b542db TST: improve flaky integration tests 2024-12-12 12:45:16 +00:00
GLaDOS
66aa5ecc78
Merge pull request #7404 from BlueWallet/renovate/react-native-menu-menu-digest
Update @react-native-menu/menu digest to 4e47b33
2024-12-12 04:48:09 +00:00
Marcos Rodriguez Velez
4a00a45d32 Update Podfile.lock 2024-12-11 23:49:26 -04:00
GLaDOS
587d1d09f4
Merge pull request #7386 from BlueWallet/renovate/react-native-safe-area-context-4.x
Update dependency react-native-safe-area-context to v4.14.1
2024-12-12 03:31:48 +00:00
renovate[bot]
821403f970
Update dependency react-native-safe-area-context to v4.14.1 2024-12-12 02:31:24 +00:00
renovate[bot]
ec5ae381e1
Update @react-native-menu/menu digest to 4e47b33 2024-12-12 02:29:16 +00:00
GLaDOS
d44e76fdde
Merge pull request #7381 from BlueWallet/renovate/react-native-svg-15.x
Update dependency react-native-svg to v15.10.1
2024-12-12 02:19:29 +00:00
GLaDOS
ab92d66b28
Merge pull request #7402 from BlueWallet/renovate/react-native-reanimated-3.x
Update dependency react-native-reanimated to v3.16.5
2024-12-12 02:19:24 +00:00
GLaDOS
fd752ffea7
Merge pull request #7388 from BlueWallet/locsync17
fix: sync language files
2024-12-11 20:07:13 +00:00
GLaDOS
83708b9ba8
Merge pull request #7394 from BlueWallet/Icon-of-the-mac-version-got-sharp-corners-now-#7379
FIX: Icon of the mac version got sharp corners now #7379
2024-12-11 20:07:08 +00:00
renovate[bot]
f37e2e213d
Update dependency react-native-reanimated to v3.16.5 2024-12-11 19:57:56 +00:00
GLaDOS
5844065e37
Merge pull request #7396 from BlueWallet/dddd
FIX: Enable notifications won't stay on #7390
2024-12-11 19:55:40 +00:00
GLaDOS
44f6d3bfe0
Merge pull request #7401 from BlueWallet/hideb
FIX: Bug: open a wallet with a hidden balance then show #7383
2024-12-11 19:55:37 +00:00
Overtorment
3f755b4bcf
Merge pull request #7400 from BlueWallet/realm 2024-12-11 19:43:51 +00:00
GLaDOS
e26f1a4476
Merge pull request #7398 from BlueWallet/translations_loc-en-json--master_pl
Updates for file loc/en.json in pl
2024-12-11 17:59:21 +00:00
Marcos Rodriguez Velez
df8c9793d8 FIX: Bug: open a wallet with a hidden balance then show #7383 2024-12-11 13:29:20 -04:00
Marcos Rodriguez Velez
52b0d69f32 FIX: In case Apple makes changes to the temp folder icloud exclusion rule 2024-12-11 13:20:13 -04:00
Marcos Rodriguez Velez
9ba2852057 OPS: Podfile 2024-12-11 13:13:59 -04:00
transifex-integration[bot]
06abd83f5b
Translate loc/en.json in pl
100% reviewed source file: 'loc/en.json'
on 'pl'.
2024-12-11 15:40:26 +00:00
Marcos Rodriguez Velez
a18ba6084c FIX: Enable notifications won't stay on #7390 2024-12-10 16:45:36 -04:00
Marcos Rodriguez Velez
f89200b52d Update Fastfile 2024-12-09 21:44:07 -04:00
Marcos Rodriguez Velez
0209c36228 FIX: Icon of the mac version got sharp corners now #7379 2024-12-09 21:38:34 -04:00
GLaDOS
a605d5f3a5
Merge pull request #7324 from BlueWallet/style
feat: new export wallet screen
2024-12-09 15:40:25 +00:00
Ivan Vershigora
cb1d827b83
fix: sync language files 2024-12-07 11:02:34 +00:00
Ivan Vershigora
32e0ecf3c9
feat: new wallet export screen 2024-12-07 10:57:44 +00:00
Ivan Vershigora
5500856abf
fix: no borderRadius: 8 for QRCODE 2024-12-07 10:57:44 +00:00
Ivan Vershigora
9c01d06212
fix: some styles 2024-12-07 10:57:43 +00:00
renovate[bot]
70731e8076
Update dependency react-native-svg to v15.10.1 2024-12-05 14:33:12 +00: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
276 changed files with 14050 additions and 8986 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
@ -97,11 +97,12 @@ jobs:
with:
name: signed-apk
path: ${{ env.APK_PATH }}
if-no-files-found: error
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,

12
Gemfile
View file

@ -2,9 +2,15 @@ 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.3.2'
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'rubyzip', '2.4.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.3.2)
securerandom (0.3.1)
rubyzip (2.4.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
rubyzip (= 2.3.2)
jwt
rubyzip (= 2.4.1)
xcodeproj (< 1.26.0)
RUBY VERSION
ruby 3.3.5p100
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.6"
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
@ -106,14 +107,12 @@
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" android:mimeType="application/octet-stream" android:pathPattern=".*\\.psbt" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" android:mimeType="image/jpeg" />
<data android:scheme="file" android:mimeType="image/png" />
<data android:scheme="file" android:mimeType="image/jpg" />
@ -123,7 +122,6 @@
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bitcoin" />
<data android:scheme="lightning" />
<data android:scheme="bluewallet" />

View file

@ -125,6 +125,13 @@
"symbol": "ÂŁ",
"country": "United Kingdom (British Pound)"
},
"HKD": {
"endPointKey": "HKD",
"locale": "zh-HK",
"source": "CoinGecko",
"symbol": "HK$",
"country": "Hong Kong (Hong Kong Dollar)"
},
"HRK": {
"endPointKey": "HRK",
"locale": "hr-HR",
@ -286,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",
@ -303,7 +317,7 @@
"RSD": {
"endPointKey": "RSD",
"locale": "sr-RS",
"source": "CoinGecko",
"source": "CoinDesk",
"symbol": "DIN",
"country": "Serbia (Serbian Dinar)"
},

View file

@ -4,32 +4,46 @@ 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() {
companion object {
private const val TAG = "BitcoinPriceWidget"
private const val SHARED_PREF_NAME = "group.io.bluewallet.bluewallet"
}
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
Log.d("BitcoinPriceWidget", "onUpdate called")
WidgetUpdateWorker.scheduleWork(context)
for (widgetId in appWidgetIds) {
Log.d(TAG, "Updating widget with ID: $widgetId")
WidgetUpdateWorker.scheduleWork(context)
}
}
override fun onEnabled(context: Context) {
super.onEnabled(context)
Log.d("BitcoinPriceWidget", "onEnabled called")
Log.d(TAG, "onEnabled called")
WidgetUpdateWorker.scheduleWork(context)
}
override fun onDisabled(context: Context) {
super.onDisabled(context)
Log.d("BitcoinPriceWidget", "onDisabled called")
Log.d(TAG, "onDisabled called")
clearCache(context)
WorkManager.getInstance(context).cancelUniqueWork(WidgetUpdateWorker.WORK_NAME)
}
private fun clearCache(context: Context) {
val sharedPref = context.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
sharedPref.edit().clear().apply() // Clear all preferences in the group
Log.d("BitcoinPriceWidget", "Cache cleared from group.io.bluewallet.bluewallet")
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
Log.d(TAG, "onDeleted called for widgets: ${appWidgetIds.joinToString()}")
}
private fun clearCache(context: Context) {
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
sharedPref.edit().clear().apply()
Log.d(TAG, "Cache cleared from $SHARED_PREF_NAME")
}
}

View file

@ -2,6 +2,7 @@ package io.bluewallet.bluewallet
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import com.bugsnag.android.Bugsnag
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
@ -11,11 +12,20 @@ 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
class MainApplication : Application(), ReactApplication {
private lateinit var sharedPref: SharedPreferences
private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { prefs, key ->
if (key == "preferredCurrency") {
prefs.edit().remove("previous_price").apply()
WidgetUpdateWorker.scheduleWork(this)
}
}
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
@ -35,27 +45,29 @@ class MainApplication : Application(), ReactApplication {
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
sharedPref = getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
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()
}
val sharedPref = applicationContext.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
initializeBugsnag()
}
// Retrieve the "donottrack" value. Default to "0" if not found.
override fun onTerminate() {
super.onTerminate()
sharedPref.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
}
private fun initializeBugsnag() {
val isDoNotTrackEnabled = sharedPref.getString("donottrack", "0")
// Check if do not track is not enabled and initialize Bugsnag if so
if (isDoNotTrackEnabled != "1") {
// Initialize Bugsnag or your error tracking here
Bugsnag.start(this)
}
}

View file

@ -2,20 +2,21 @@ package io.bluewallet.bluewallet
import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
object MarketAPI {
private const val TAG = "MarketAPI"
private val client = OkHttpClient()
var baseUrl: String? = null
fun fetchPrice(context: Context, currency: String): String? {
suspend fun fetchPrice(context: Context, currency: String): String? {
return try {
// Load the JSON data from the assets
val fiatUnitsJson = context.assets.open("fiatUnits.json").bufferedReader().use { it.readText() }
val json = JSONObject(fiatUnitsJson)
val currencyInfo = json.getJSONObject(currency)
@ -25,26 +26,16 @@ object MarketAPI {
val urlString = buildURLString(source, endPointKey)
Log.d(TAG, "Fetching price from URL: $urlString")
val url = URL(urlString)
val urlConnection = url.openConnection() as HttpURLConnection
urlConnection.requestMethod = "GET"
urlConnection.connect()
val request = Request.Builder().url(urlString).build()
val response = withContext(Dispatchers.IO) { client.newCall(request).execute() }
val responseCode = urlConnection.responseCode
if (responseCode != 200) {
Log.e(TAG, "Failed to fetch price. Response code: $responseCode")
if (!response.isSuccessful) {
Log.e(TAG, "Failed to fetch price. Response code: ${response.code}")
return null
}
val reader = InputStreamReader(urlConnection.inputStream)
val jsonResponse = StringBuilder()
val buffer = CharArray(1024)
var read: Int
while (reader.read(buffer).also { read = it } != -1) {
jsonResponse.append(buffer, 0, read)
}
parseJSONBasedOnSource(jsonResponse.toString(), source, endPointKey)
val jsonResponse = response.body?.string() ?: return null
parseJSONBasedOnSource(jsonResponse, source, endPointKey)
} catch (e: Exception) {
Log.e(TAG, "Error fetching price", e)
null
@ -65,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()}"
}
}
}
@ -82,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

@ -1,8 +1,10 @@
package io.bluewallet.bluewallet
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.util.Log
import android.view.View
@ -13,8 +15,10 @@ import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
companion object {
const val TAG = "WidgetUpdateWorker"
@ -35,66 +39,57 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
}
private lateinit var sharedPref: SharedPreferences
private lateinit var preferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener
override fun doWork(): Result {
override suspend fun doWork(): Result {
Log.d(TAG, "Widget update worker running")
sharedPref = applicationContext.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
registerPreferenceChangeListener()
val appWidgetManager = AppWidgetManager.getInstance(applicationContext)
val thisWidget = ComponentName(applicationContext, BitcoinPriceWidget::class.java)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
val views = RemoteViews(applicationContext.packageName, R.layout.widget_layout)
val intent = Intent(applicationContext, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
views.setOnClickPendingIntent(R.id.widget_layout, pendingIntent)
// Show loading indicator
views.setViewVisibility(R.id.loading_indicator, View.VISIBLE)
views.setViewVisibility(R.id.price_value, View.GONE)
views.setViewVisibility(R.id.last_updated_label, View.GONE)
views.setViewVisibility(R.id.last_updated_time, View.GONE)
views.setViewVisibility(R.id.price_arrow_container, View.GONE)
appWidgetManager.updateAppWidget(appWidgetIds, views)
val preferredCurrency = sharedPref.getString("preferredCurrency", null) ?: "USD"
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", null) ?: "en-US"
val previousPrice = sharedPref.getString("previous_price", null)
val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
fetchPrice(preferredCurrency) { fetchedPrice, error ->
handlePriceResult(
appWidgetManager, appWidgetIds, views, sharedPref,
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale, error
)
}
val fetchedPrice = fetchPrice(preferredCurrency)
handlePriceResult(
appWidgetManager, appWidgetIds, views, sharedPref,
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
)
return Result.success()
}
private fun registerPreferenceChangeListener() {
preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == "preferredCurrency" || key == "preferredCurrencyLocale" || key == "previous_price") {
Log.d(TAG, "Preference changed: $key")
updateWidgetOnPreferenceChange()
}
}
sharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
}
override fun onStopped() {
super.onStopped()
sharedPref.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
}
private fun updateWidgetOnPreferenceChange() {
val appWidgetManager = AppWidgetManager.getInstance(applicationContext)
val thisWidget = ComponentName(applicationContext, BitcoinPriceWidget::class.java)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
val views = RemoteViews(applicationContext.packageName, R.layout.widget_layout)
val preferredCurrency = sharedPref.getString("preferredCurrency", null) ?: "USD"
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", null) ?: "en-US"
val previousPrice = sharedPref.getString("previous_price", null)
val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
fetchPrice(preferredCurrency) { fetchedPrice, error ->
handlePriceResult(
appWidgetManager, appWidgetIds, views, sharedPref,
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale, error
)
private suspend fun fetchPrice(currency: String?): String? {
return withContext(Dispatchers.IO) {
MarketAPI.fetchPrice(applicationContext, currency ?: "USD")
}
}
@ -107,24 +102,27 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
previousPrice: String?,
currentTime: String,
preferredCurrency: String?,
preferredCurrencyLocale: String?,
error: String?
preferredCurrencyLocale: String?
) {
val isPriceFetched = fetchedPrice != null
val isPriceCached = previousPrice != null
if (error != null || !isPriceFetched) {
Log.e(TAG, "Error fetching price: $error")
if (!isPriceFetched) {
Log.e(TAG, "Error fetching price.")
if (!isPriceCached) {
showLoadingError(views)
} else {
displayCachedPrice(views, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale)
}
} else {
displayFetchedPrice(
views, fetchedPrice!!, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
)
savePrice(sharedPref, fetchedPrice)
if (fetchedPrice != null) {
displayFetchedPrice(
views, fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
)
}
if (fetchedPrice != null) {
savePrice(sharedPref, fetchedPrice)
}
}
appWidgetManager.updateAppWidget(appWidgetIds, views)
@ -132,7 +130,7 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
private fun showLoadingError(views: RemoteViews) {
views.apply {
setViewVisibility(R.id.loading_indicator, View.VISIBLE)
setViewVisibility(R.id.loading_indicator, View.GONE)
setViewVisibility(R.id.price_value, View.GONE)
setViewVisibility(R.id.last_updated_label, View.GONE)
setViewVisibility(R.id.last_updated_time, View.GONE)
@ -216,15 +214,6 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
return currencyFormat
}
private fun fetchPrice(currency: String?, callback: (String?, String?) -> Unit) {
val price = MarketAPI.fetchPrice(applicationContext, currency ?: "USD")
if (price == null) {
callback(null, "Failed to fetch price")
} else {
callback(price, null)
}
}
private fun savePrice(sharedPref: SharedPreferences, price: String) {
sharedPref.edit().putString("previous_price", price).apply()
}

View file

@ -13,13 +13,14 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
android:visibility="visible"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end">
android:gravity="end"
android:layout_gravity="end">
<TextView
android:id="@+id/last_updated_label"
style="@style/WidgetTextSecondary"
@ -29,69 +30,86 @@
android:textSize="12sp"
android:textStyle="bold"
android:layout_marginEnd="8dp"
android:visibility="gone"/>
android:visibility="gone"
android:layout_gravity="end"/>
<TextView
android:id="@+id/last_updated_time"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:text="--:--"
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginTop="2dp"
android:visibility="gone"/>
android:visibility="gone"
android:layout_gravity="end"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end">
android:gravity="end"
android:layout_gravity="end">
<TextView
android:id="@+id/price_value"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:visibility="gone"/>
<LinearLayout
android:id="@+id/price_arrow_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:visibility="gone">
<ImageView
android:id="@+id/price_arrow"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/previous_price_label"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="From:"
android:textSize="12sp"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/previous_price"
style="@style/WidgetTextPrimary"
android:layout_marginBottom="8dp"
android:autoSizeMaxTextSize="24sp"
android:autoSizeMinTextSize="12sp"
android:autoSizeStepGranularity="2sp"
android:autoSizeTextType="uniform"
android:duplicateParentState="false"
android:editable="false"
android:gravity="end"
android:lines="1"
android:text="Loading..."
android:textSize="24sp"
android:textStyle="bold"
android:visibility="gone" />
<LinearLayout
android:id="@+id/price_arrow_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"/>
</LinearLayout></LinearLayout>
android:orientation="horizontal"
android:gravity="end"
android:visibility="gone"
android:layout_gravity="end">
<ImageView
android:id="@+id/price_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="8dp"
android:layout_gravity="end"/>
<TextView
android:id="@+id/previous_price_label"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="From:"
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:layout_gravity="end"/>
<TextView
android:id="@+id/previous_price"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="--"
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:layout_gravity="end"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

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
}

2
android/gradlew vendored
View file

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

@ -1,4 +1,3 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import BigNumber from 'bignumber.js';
import * as bitcoin from 'bitcoinjs-lib';
import DefaultPreference from 'react-native-default-preference';
@ -9,6 +8,9 @@ import { LegacyWallet, SegwitBech32Wallet, SegwitP2SHWallet, TaprootWallet } fro
import presentAlert from '../components/Alert';
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');
@ -67,17 +69,11 @@ type MempoolTransaction = {
fee: number;
};
type Peer =
| {
host: string;
ssl: string;
tcp?: undefined;
}
| {
host: string;
tcp: string;
ssl?: undefined;
};
type Peer = {
host: string;
ssl?: number;
tcp?: number;
};
export const ELECTRUM_HOST = 'electrum_host';
export const ELECTRUM_TCP_PORT = 'electrum_tcp_port';
@ -85,16 +81,20 @@ export const ELECTRUM_SSL_PORT = 'electrum_ssl_port';
export const ELECTRUM_SERVER_HISTORY = 'electrum_server_history';
const ELECTRUM_CONNECTION_DISABLED = 'electrum_disabled';
const storageKey = 'ELECTRUM_PEERS';
const defaultPeer = { host: 'electrum1.bluewallet.io', ssl: '443' };
const defaultPeer = { host: 'electrum1.bluewallet.io', ssl: 443 };
export const hardcodedPeers: Peer[] = [
{ host: 'mainnet.foundationdevices.com', ssl: '50002' },
// { host: 'bitcoin.lukechilds.co', ssl: '50002' },
{ host: 'mainnet.foundationdevices.com', ssl: 50002 },
// { host: 'bitcoin.lukechilds.co', ssl: 50002 },
// { host: 'electrum.jochen-hoenicke.de', ssl: '50006' },
{ host: 'electrum1.bluewallet.io', ssl: '443' },
{ host: 'electrum.acinq.co', ssl: '50002' },
{ host: 'electrum.bitaroo.net', ssl: '50002' },
{ host: 'electrum1.bluewallet.io', ssl: 443 },
{ host: 'electrum.acinq.co', ssl: 50002 },
{ host: 'electrum.bitaroo.net', ssl: 50002 },
];
export const suggestedServers: Peer[] = hardcodedPeers.map(peer => ({
...peer,
}));
let mainClient: typeof ElectrumClient | undefined;
let mainConnected: boolean = false;
let wasConnectedAtLeastOnce: boolean = false;
@ -131,28 +131,71 @@ async function _getRealm() {
schema,
path,
encryptionKey,
excludeFromIcloudBackup: true,
});
return _realm;
}
export const getPreferredServer = async (): Promise<ElectrumServerItem | undefined> => {
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 });
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;
}
};
export const removePreferredServer = async () => {
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> {
let result;
try {
const savedValue = await AsyncStorage.getItem(ELECTRUM_CONNECTION_DISABLED);
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const savedValue = await DefaultPreference.get(ELECTRUM_CONNECTION_DISABLED);
console.log('Getting Electrum connection disabled state:', savedValue);
if (savedValue === null) {
result = false;
} else {
result = savedValue;
}
} catch {
} catch (error) {
console.error('Error getting Electrum connection disabled state:', error);
result = false;
}
return !!result;
}
export async function setDisabled(disabled = true) {
return AsyncStorage.setItem(ELECTRUM_CONNECTION_DISABLED, disabled ? '1' : '');
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
console.log('Setting Electrum connection disabled state to:', disabled);
return DefaultPreference.set(ELECTRUM_CONNECTION_DISABLED, disabled ? '1' : '');
}
function getCurrentPeer() {
@ -170,23 +213,31 @@ function getNextPeer() {
}
async function getSavedPeer(): Promise<Peer | null> {
const host = await AsyncStorage.getItem(ELECTRUM_HOST);
const tcpPort = await AsyncStorage.getItem(ELECTRUM_TCP_PORT);
const sslPort = await AsyncStorage.getItem(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);
if (!host) {
console.log('Getting saved peer:', { host, tcpPort, sslPort });
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: sslPort };
}
if (tcpPort) {
return { host, tcp: tcpPort };
}
return null;
}
export async function connectMain(): Promise<void> {
@ -200,22 +251,7 @@ export async function connectMain(): Promise<void> {
usingPeer = savedPeer;
}
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 ?? '');
} else {
await DefaultPreference.set(ELECTRUM_HOST, usingPeer.host);
await DefaultPreference.set(ELECTRUM_TCP_PORT, usingPeer.tcp ?? '');
await DefaultPreference.set(ELECTRUM_SSL_PORT, usingPeer.ssl ?? '');
}
} catch (e) {
// Must be running on Android
console.log(e);
}
console.log('Using peer:', JSON.stringify(usingPeer));
try {
console.log('begin connection:', JSON.stringify(usingPeer));
@ -227,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
@ -275,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 {
@ -291,6 +331,67 @@ 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,
options: { cancelable: true },
});
});
}
const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
if (await isDisabled()) {
console.log(
@ -298,6 +399,7 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
);
return;
}
presentAlert({
allowRepeat: false,
title: loc.errors.network,
@ -310,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',
@ -318,39 +421,14 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
{
text: loc.settings.electrum_reset,
onPress: () => {
presentAlert({
title: loc.settings.electrum_reset,
message: loc.settings.electrum_reset_to_default,
buttons: [
{
text: loc._.cancel,
style: 'cancel',
onPress: () => {},
},
{
text: loc._.ok,
style: 'destructive',
onPress: async () => {
await AsyncStorage.setItem(ELECTRUM_HOST, '');
await AsyncStorage.setItem(ELECTRUM_TCP_PORT, '');
await AsyncStorage.setItem(ELECTRUM_SSL_PORT, '');
try {
await DefaultPreference.setName('group.io.bluewallet.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
}
presentAlert({ message: loc.settings.electrum_saved });
setTimeout(connectMain, 500);
},
},
],
options: { cancelable: true },
presentResetToDefaultsAlert().then(result => {
if (result) {
connectionAttempt = 0;
mainClient?.close();
mainClient = undefined;
setTimeout(connectMain, 500);
}
});
connectionAttempt = 0;
mainClient.close() && mainClient.close();
},
style: 'destructive',
},
@ -358,7 +436,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
text: loc._.cancel,
onPress: () => {
connectionAttempt = 0;
mainClient.close() && mainClient.close();
mainClient?.close();
mainClient = undefined;
},
style: 'cancel',
},
@ -376,13 +455,18 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function getRandomDynamicPeer(): Promise<Peer> {
try {
let peers = JSON.parse((await AsyncStorage.getItem(storageKey)) as string);
let peers = JSON.parse((await DefaultPreference.get(storageKey)) as string);
peers = peers.sort(() => Math.random() - 0.5); // shuffle
for (const peer of peers) {
const ret = {
host: peer[1] as string,
tcp: '',
};
const ret: Peer = { host: peer[0], ssl: peer[1] };
ret.host = peer[1];
if (peer[1] === 's') {
ret.ssl = peer[2];
} else {
ret.tcp = peer[2];
}
for (const item of peer[2]) {
if (item.startsWith('t')) {
ret.tcp = item.replace('t', '');
@ -398,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 () {
@ -882,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 } from 'react-native-image-picker';
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';
@ -16,65 +15,54 @@ const _sanitizeFileName = (fileName: string) => {
};
const _shareOpen = async (filePath: string, showShareDialog: boolean = false) => {
return await Share.open({
url: 'file://' + filePath,
saveToFiles: isDesktop || !showShareDialog,
// @ts-ignore: Website claims this propertie exists, but TS cant find it. Send anyways.
useInternalStorage: Platform.OS === 'android',
failOnCancel: false,
})
.catch(error => {
console.log(error);
// If user cancels sharing, we dont want to show an error. for some reason we get 'CANCELLED' string as error
if (error.message !== 'CANCELLED') {
presentAlert({ message: error.message });
}
})
.finally(() => {
RNFS.unlink(filePath);
try {
await Share.open({
url: 'file://' + filePath,
saveToFiles: isDesktop || !showShareDialog,
// @ts-ignore: Website claims this propertie exists, but TS cant find it. Send anyways.
useInternalStorage: Platform.OS === 'android',
failOnCancel: false,
});
} catch (error: any) {
console.log(error);
// If user cancels sharing, we dont want to show an error. for some reason we get 'CANCELLED' string as error
if (error.message !== 'CANCELLED') {
presentAlert({ message: error.message });
}
} finally {
await RNFS.unlink(filePath);
}
};
/**
* 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) {
const sanitizedFileName = _sanitizeFileName(fileName);
if (Platform.OS === 'ios') {
const filePath = RNFS.TemporaryDirectoryPath + `/${sanitizedFileName}`;
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;
request(permissionType).then(async result => {
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 });
try {
if (Platform.OS === 'ios') {
const filePath = `${RNFS.TemporaryDirectoryPath}/${sanitizedFileName}`;
await RNFS.writeFile(filePath, contents);
await _shareOpen(filePath, showShareDialog);
} else if (Platform.OS === 'android') {
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 });
}
};
@ -110,41 +98,45 @@ 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;
}
};
export const showImagePickerAndReadImage = (): Promise<string | undefined> => {
return new Promise((resolve, reject) =>
launchImageLibrary(
{
mediaType: 'photo',
maxHeight: 800,
maxWidth: 600,
selectionLimit: 1,
},
response => {
if (!response.didCancel) {
const asset = response.assets?.[0] ?? {};
if (asset.uri) {
RNQRGenerator.detect({
uri: decodeURI(asset.uri.toString()),
})
.then(result => {
if (result) {
resolve(result.values[0]);
}
})
.catch(error => {
console.error(error);
reject(new Error(loc.send.qr_error_no_qrcode));
});
export const showImagePickerAndReadImage = async (): Promise<string | undefined> => {
try {
const response: ImagePickerResponse = await launchImageLibrary({
mediaType: 'photo',
maxHeight: 800,
maxWidth: 600,
selectionLimit: 1,
});
if (response.didCancel) {
return undefined;
} else if (response.errorCode) {
throw new Error(response.errorMessage);
} else if (response.assets) {
try {
const uri = response.assets[0].uri;
if (uri) {
const result = await RNQRGenerator.detect({ uri: decodeURI(uri.toString()) });
if (result?.values.length > 0) {
return result?.values[0];
}
}
},
),
);
throw new Error(loc.send.qr_error_no_qrcode);
} catch (error) {
console.error(error);
presentAlert({ message: loc.send.qr_error_no_qrcode });
}
}
return undefined;
} catch (error: any) {
console.error(error);
throw error;
}
};
export const showFilePickerAndReadFile = async function (): Promise<{ data: string | false; uri: string | false }> {
@ -165,45 +157,24 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri
});
if (!res.fileCopyUri) {
// to make ts happy, should not need this check here
presentAlert({ message: 'Picking and caching a file failed' });
return { data: false, uri: false };
}
const fileCopyUri = decodeURI(res.fileCopyUri);
let file;
if (res.fileCopyUri.toLowerCase().endsWith('.psbt')) {
// this is either binary file from ElectrumDesktop OR string file with base64 string in there
file = await _readPsbtFileIntoBase64(fileCopyUri);
return { data: file, uri: decodeURI(res.fileCopyUri) };
const file = await _readPsbtFileIntoBase64(fileCopyUri);
return { data: file, uri: fileCopyUri };
}
if (res.type === DocumentPicker.types.images || res.type?.startsWith('image/')) {
return new Promise(resolve => {
if (!res.fileCopyUri) {
// to make ts happy, should not need this check here
presentAlert({ message: 'Picking and caching a file failed' });
resolve({ data: false, uri: false });
return;
}
const uri2 = res.fileCopyUri.replace('file://', '');
RNQRGenerator.detect({
uri: decodeURI(uri2),
})
.then(result => {
if (result) {
resolve({ data: result.values[0], uri: fileCopyUri });
}
})
.catch(error => {
console.error(error);
resolve({ data: false, uri: false });
});
});
return await handleImageFile(fileCopyUri);
}
file = await RNFS.readFile(fileCopyUri);
const file = await RNFS.readFile(fileCopyUri);
return { data: file, uri: fileCopyUri };
} catch (err: any) {
if (!DocumentPicker.isCancel(err)) {
@ -213,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

@ -2,10 +2,11 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
import PushNotificationIOS from '@react-native-community/push-notification-ios';
import { AppState, Platform } from 'react-native';
import { getApplicationName, getSystemName, getSystemVersion, getVersion, hasGmsSync, hasHmsSync } from 'react-native-device-info';
import { checkNotifications, requestNotifications } from 'react-native-permissions';
import { checkNotifications, requestNotifications, RESULTS } from 'react-native-permissions';
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';
@ -14,6 +15,19 @@ export const NOTIFICATIONS_NO_AND_DONT_ASK_FLAG = 'NOTIFICATIONS_NO_AND_DONT_ASK
let alreadyConfigured = false;
let baseURI = groundControlUri;
const deepClone = obj => JSON.parse(JSON.stringify(obj));
const checkAndroidNotificationPermission = async () => {
try {
const { status } = await checkNotifications();
console.debug('Notification permission check:', status);
return status === RESULTS.GRANTED;
} catch (err) {
console.error('Failed to check notification permission:', err);
return false;
}
};
export const checkNotificationPermissionStatus = async () => {
try {
const { status } = await checkNotifications();
@ -28,15 +42,14 @@ export const checkNotificationPermissionStatus = async () => {
let currentPermissionStatus = 'unavailable';
const handleAppStateChange = async nextAppState => {
if (nextAppState === 'active') {
const newPermissionStatus = await checkNotificationPermissionStatus();
if (newPermissionStatus !== currentPermissionStatus) {
currentPermissionStatus = newPermissionStatus;
if (newPermissionStatus === 'granted') {
// Re-initialize notifications if permissions are granted
await initializeNotifications();
} else {
// Optionally, handle the case where permissions are disabled (e.g., disable in-app notifications)
console.warn('Notifications have been disabled at the system level.');
const isDisabledByUser = (await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG)) === 'true';
if (!isDisabledByUser) {
const newPermissionStatus = await checkNotificationPermissionStatus();
if (newPermissionStatus !== currentPermissionStatus) {
currentPermissionStatus = newPermissionStatus;
if (newPermissionStatus === 'granted') {
await initializeNotifications();
}
}
}
}
@ -64,25 +77,34 @@ export const cleanUserOptOutFlag = async () => {
* @returns {Promise<boolean>}
*/
export const tryToObtainPermissions = async () => {
if (!isNotificationsCapable) return false;
console.debug('tryToObtainPermissions: Starting user-triggered permission request');
try {
const token = await getPushToken();
if (token) {
if (!alreadyConfigured) {
await configureNotifications();
}
return true;
}
} catch (error) {
console.error('Failed to obtain permissions:', error.message);
if (error.code) {
console.debug('Error code:', error.code);
}
if (!isNotificationsCapable) {
console.debug('tryToObtainPermissions: Device not capable');
return false;
}
return configureNotifications();
try {
const rationale = {
title: loc.settings.notifications,
message: loc.notifications.would_you_like_to_receive_notifications,
buttonPositive: loc._.ok,
buttonNegative: loc.notifications.no_and_dont_ask,
};
const { status } = await requestNotifications(
['alert', 'sound', 'badge'],
Platform.OS === 'android' && Platform.Version < 33 ? rationale : undefined,
);
if (status !== RESULTS.GRANTED) {
console.debug('tryToObtainPermissions: Permission denied');
return false;
}
return configureNotifications();
} catch (error) {
console.error('Error requesting notification permissions:', error);
return false;
}
};
/**
* Submits onchain bitcoin addresses and ln invoice preimage hashes to GroundControl server, so later we could
@ -94,6 +116,12 @@ export const tryToObtainPermissions = async () => {
* @returns {Promise<object>} Response object from API rest call
*/
export const majorTomToGroundControl = async (addresses, hashes, txids) => {
console.debug('majorTomToGroundControl: Starting notification registration', {
addressCount: addresses?.length,
hashCount: hashes?.length,
txidCount: txids?.length,
});
try {
const noAndDontAskFlag = await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG);
if (noAndDontAskFlag === 'true') {
@ -106,6 +134,7 @@ export const majorTomToGroundControl = async (addresses, hashes, txids) => {
}
const pushToken = await getPushToken();
console.debug('majorTomToGroundControl: Retrieved push token:', !!pushToken);
if (!pushToken || !pushToken.token || !pushToken.os) {
return;
}
@ -120,6 +149,7 @@ export const majorTomToGroundControl = async (addresses, hashes, txids) => {
let response;
try {
console.debug('majorTomToGroundControl: Sending request to:', `${baseURI}/majorTomToGroundControl`);
response = await fetch(`${baseURI}/majorTomToGroundControl`, {
method: 'POST',
headers: _getHeaders(),
@ -160,11 +190,16 @@ export const majorTomToGroundControl = async (addresses, hashes, txids) => {
* @returns {Promise<Object>}
*/
export const checkPermissions = async () => {
return new Promise(function (resolve) {
PushNotification.checkPermissions(result => {
resolve(result);
try {
return new Promise(function (resolve) {
PushNotification.checkPermissions(result => {
resolve(result);
});
});
});
} catch (error) {
console.error('Error checking permissions:', error);
throw error;
}
};
/**
@ -199,7 +234,7 @@ export const setLevels = async levelAll => {
await Promise.all([
new Promise(resolve => PushNotification.removeAllDeliveredNotifications(resolve)),
new Promise(resolve => PushNotification.setApplicationIconBadgeNumber(0, resolve)),
new Promise(resolve => PushNotification.removePendingNotificationRequests(resolve)),
new Promise(resolve => PushNotification.cancelAllLocalNotifications(resolve)),
AsyncStorage.setItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG, 'true'),
]);
console.debug('Notifications disabled successfully');
@ -228,12 +263,19 @@ export const addNotification = async notification => {
};
const postTokenConfig = async () => {
console.debug('postTokenConfig: Starting token configuration');
const pushToken = await getPushToken();
if (!pushToken || !pushToken.token || !pushToken.os) return;
console.debug('postTokenConfig: Retrieved push token:', !!pushToken);
if (!pushToken || !pushToken.token || !pushToken.os) {
console.debug('postTokenConfig: Invalid token or missing OS info');
return;
}
try {
const lang = (await AsyncStorage.getItem('lang')) || 'en';
const appVersion = getSystemName() + ' ' + getSystemVersion() + ';' + getApplicationName() + ' ' + getVersion();
console.debug('postTokenConfig: Posting configuration', { lang, appVersion });
await fetch(`${baseURI}/setTokenConfiguration`, {
method: 'POST',
@ -253,8 +295,13 @@ const postTokenConfig = async () => {
};
const _setPushToken = async token => {
token = JSON.stringify(token);
return AsyncStorage.setItem(PUSH_TOKEN, token);
try {
token = JSON.stringify(token);
return await AsyncStorage.setItem(PUSH_TOKEN, token);
} catch (error) {
console.error('Error setting push token:', error);
throw error;
}
};
/**
@ -263,103 +310,82 @@ const _setPushToken = async token => {
* @returns {Promise<boolean>}
*/
export const configureNotifications = async onProcessNotifications => {
if (alreadyConfigured) {
console.debug('configureNotifications: Already configured, skipping');
return true;
}
return new Promise(resolve => {
const configure = async () => {
const existingToken = await getPushToken();
if (existingToken) {
alreadyConfigured = true;
console.debug('Notifications already configured with existing token.');
if (__DEV__) {
console.debug('Existing Token:', existingToken);
}
resolve(true);
const handleRegistration = async token => {
if (__DEV__) {
console.debug('configureNotifications: Token received:', token);
}
alreadyConfigured = true;
await _setPushToken(token);
resolve(true);
};
const handleNotification = async notification => {
// Deep clone to avoid modifying the original object
const payload = deepClone({
...notification,
...notification.data,
});
if (notification.data?.data) {
const validData = Object.fromEntries(Object.entries(notification.data.data).filter(([_, value]) => value != null));
Object.assign(payload, validData);
}
payload.data = undefined;
if (!payload.title && !payload.message) {
console.warn('Notification missing required fields:', payload);
return;
}
const rationale = {
title: loc.settings.notifications,
message: loc.notifications.would_you_like_to_receive_notifications,
buttonPositive: loc._.ok,
buttonNegative: loc.notifications.no_and_dont_ask,
};
await addNotification(payload);
notification.finish(PushNotificationIOS.FetchResult.NoData);
const requestPermissions = Platform.OS === 'ios';
if (payload.foreground && onProcessNotifications) {
await onProcessNotifications();
}
};
requestNotifications(['alert', 'sound', 'badge'], Platform.OS === 'android' ? rationale : undefined)
.then(({ status }) => {
if (status === 'granted') {
console.debug('Notification permissions granted.');
PushNotification.configure({
onRegister: async token => {
console.debug('TOKEN:', token);
if (__DEV__) {
console.debug('New Token:', token);
}
alreadyConfigured = true;
await _setPushToken(token);
resolve(true);
},
onNotification: async notification => {
// Deep clone to avoid modifying the original notification
const payload = structuredClone({
...notification,
...notification.data,
});
const configure = async () => {
try {
const { status } = await checkNotifications();
if (status !== RESULTS.GRANTED) {
console.debug('configureNotifications: Permissions not granted');
return resolve(false);
}
if (notification.data?.data) {
// Validate data before merging
const validData = {};
for (const [key, value] of Object.entries(notification.data.data)) {
if (value != null) {
validData[key] = value;
}
}
Object.assign(payload, validData);
}
payload.data = undefined;
const existingToken = await getPushToken();
if (existingToken) {
alreadyConfigured = true;
console.debug('Notifications already configured with existing token');
return resolve(true);
}
// Ensure required fields exist
if (!payload.title && !payload.message) {
console.warn('Notification missing required fields:', payload);
return;
}
console.debug('Received Push Notification Payload:', payload);
await addNotification(payload);
notification.finish(PushNotificationIOS.FetchResult.NoData);
if (payload.foreground && onProcessNotifications) {
await onProcessNotifications();
}
},
onRegistrationError: err => {
console.error(err.message, err);
resolve(false);
},
permissions: { alert: true, badge: true, sound: true },
popInitialNotification: true,
requestPermissions,
});
} else {
console.warn('Notification permissions not granted.');
PushNotification.configure({
onRegister: handleRegistration,
onNotification: handleNotification,
onRegistrationError: error => {
console.error('Registration error:', error);
resolve(false);
}
})
.catch(error => {
console.error('Failed to request notifications permission:', error);
resolve(false);
},
permissions: { alert: true, badge: true, sound: true },
popInitialNotification: true,
});
} catch (error) {
console.error('Error in configure:', error);
resolve(false);
}
};
configure();
});
};
const _sleep = async ms => {
return new Promise(resolve => setTimeout(resolve, ms));
};
/**
* Validates whether the provided GroundControl URI is valid by pinging it.
*
@ -367,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';
@ -401,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 {};
}
};
/**
@ -481,16 +502,21 @@ export const clearStoredNotifications = async () => {
};
export const getDeliveredNotifications = () => {
return new Promise(resolve => {
PushNotification.getDeliveredNotifications(notifications => resolve(notifications));
});
try {
return new Promise(resolve => {
PushNotification.getDeliveredNotifications(notifications => resolve(notifications));
});
} catch (error) {
console.error('Error getting delivered notifications:', error);
throw error;
}
};
export const removeDeliveredNotifications = (identifiers = []) => {
PushNotification.removeDeliveredNotifications(identifiers);
};
export const setApplicationIconBadgeNumber = function (badges) {
export const setApplicationIconBadgeNumber = badges => {
PushNotification.setApplicationIconBadgeNumber(badges);
};
@ -503,12 +529,12 @@ export const getDefaultUri = () => {
};
export const saveUri = async uri => {
baseURI = uri || groundControlUri;
try {
baseURI = uri || groundControlUri;
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, baseURI);
} catch (storageError) {
console.error('Failed to reset URI:', storageError);
throw storageError;
} catch (error) {
console.error('Error saving URI:', error);
throw error;
}
};
@ -531,8 +557,20 @@ export const getSavedUri = async () => {
};
export const isNotificationsEnabled = async () => {
const levels = await getLevels();
return !!(await getPushToken()) && !!levels.level_all;
try {
const levels = await getLevels();
const token = await getPushToken();
const isDisabledByUser = (await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG)) === 'true';
// Return true only if we have all requirements and user hasn't opted out
return !isDisabledByUser && !!token && !!levels.level_all;
} catch (error) {
console.log('Error checking notification levels:', error);
if (error instanceof SyntaxError) {
throw error;
}
return false;
}
};
export const getStoredNotifications = async () => {
@ -557,8 +595,11 @@ export const getStoredNotifications = async () => {
// on app launch (load module):
export const initializeNotifications = async onProcessNotifications => {
console.debug('initializeNotifications: Starting initialization');
try {
const noAndDontAskFlag = await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG);
console.debug('initializeNotifications: No ask flag status:', noAndDontAskFlag);
if (noAndDontAskFlag === 'true') {
console.warn('User has opted out of notifications.');
return;
@ -567,25 +608,37 @@ export const initializeNotifications = async onProcessNotifications => {
const baseUriStored = await AsyncStorage.getItem(GROUNDCONTROL_BASE_URI);
baseURI = baseUriStored || groundControlUri;
console.debug('Base URI set to:', baseURI);
} catch (e) {
console.error('Failed to load custom URI, falling back to default', e);
baseURI = groundControlUri;
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, groundControlUri).catch(err => console.error('Failed to reset URI:', err));
}
setApplicationIconBadgeNumber(0);
setApplicationIconBadgeNumber(0);
try {
// Only check permissions, never request
currentPermissionStatus = await checkNotificationPermissionStatus();
console.warn('currentPermissionStatus', currentPermissionStatus);
if (currentPermissionStatus === 'granted' && (await getPushToken())) {
console.debug('Permissions granted and push token exists. Configuring notifications...');
await configureNotifications(onProcessNotifications);
await postTokenConfig();
console.debug('initializeNotifications: Permission status:', currentPermissionStatus);
// Handle Android 13+ permissions differently
const canProceed =
Platform.OS === 'android'
? isNotificationsCapable && (await checkAndroidNotificationPermission())
: currentPermissionStatus === 'granted';
if (canProceed) {
console.debug('initializeNotifications: Can proceed with notification setup');
const token = await getPushToken();
if (token) {
console.debug('initializeNotifications: Existing token found, configuring');
await configureNotifications(onProcessNotifications);
await postTokenConfig();
} else {
console.debug('initializeNotifications: No token found, will request permissions');
await tryToObtainPermissions();
}
} else {
console.warn('Notifications are disabled at the system level.');
console.debug('Notifications require user action to enable');
}
} catch (error) {
console.error('Failed to initialize notifications:', error);
baseURI = groundControlUri;
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, groundControlUri).catch(err => console.error('Failed to reset URI:', err));
}
};

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

@ -285,6 +285,7 @@ export class BlueApp {
schema,
path,
encryptionKey,
excludeFromIcloudBackup: true,
});
}
@ -327,6 +328,7 @@ export class BlueApp {
schema,
path,
encryptionKey,
excludeFromIcloudBackup: true,
});
}

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(),
@ -413,6 +412,12 @@ class DeeplinkSchemaMatch {
}
static bip21encode(address: string, options: TOptions): string {
// uppercase address if bech32 to satisfy BIP_0173
const isBech32 = address.startsWith('bc1');
if (isBech32) {
address = address.toUpperCase();
}
for (const key in options) {
if (key === 'label' && String(options[key]).replace(' ', '').length === 0) {
delete options[key];

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

@ -348,11 +348,31 @@ const startImport = (
// maybe its a watch-only address?
yield { progress: 'watch only' };
const watchOnly = new WatchOnlyWallet();
watchOnly.setSecret(text);
if (watchOnly.valid()) {
await fetch(watchOnly, true);
yield { wallet: watchOnly };
const wo1 = new WatchOnlyWallet();
wo1.setSecret(text);
if (wo1.valid()) {
wo1.init();
if (text.startsWith('xpub')) {
// for xpub we also check ypub and zpub. If any of them was used, we import it.
let found = false;
const pubs = [text, wo1._xpubToYpub(text), wo1._xpubToZpub(text)];
for (const pub of pubs) {
const wo2 = new WatchOnlyWallet();
wo2.setSecret(pub);
wo2.init();
if (await wasUsed(wo2)) {
yield { wallet: wo2 };
found = true;
}
}
if (!found) {
await fetch(wo1, true);
yield { wallet: wo1 };
}
} else {
await fetch(wo1, true);
yield { wallet: wo1 };
}
}
// electrum p2wpkh-p2sh

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) {
@ -1417,6 +1420,12 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
if (!this.allowBIP47()) {
return false;
}
try {
// watch-only wallet will throw an error here
this.getDerivationPath();
} catch (_) {
return false;
}
// only check BIP47 if derivation path is regular, otherwise too many wallets will be found
if (!["m/84'/0'/0'", "m/44'/0'/0'", "m/49'/0'/0'"].includes(this.getDerivationPath() as string)) {
return false;

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

@ -629,7 +629,11 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
hexFingerprint = Buffer.from(hexFingerprint, 'hex').toString('hex');
}
const path = 'm/' + m[1].split('/').slice(1).join('/').replace(/[h]/g, "'");
let path = 'm/' + m[1].split('/').slice(1).join('/').replace(/[h]/g, "'");
if (path === 'm/') {
// not considered valid by Bip32 lib
path = 'm/0';
}
let xpub = m[2];
if (xpub.indexOf('/') !== -1) {
xpub = xpub.substr(0, xpub.indexOf('/'));

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';
@ -88,6 +102,12 @@ export type LightningTransaction = {
expire_time?: number;
ispaid?: boolean;
walletID?: string;
value?: number;
amt?: number;
fee?: number;
payment_preimage?: string;
payment_request?: string;
description?: string;
};
export type Transaction = {

View file

@ -312,4 +312,9 @@ export class WatchOnlyWallet extends LegacyWallet {
if (this._hdWalletInstance) return this._hdWalletInstance.isSegwit();
return super.isSegwit();
}
wasEverUsed(): Promise<boolean> {
if (this._hdWalletInstance) return this._hdWalletInstance.wasEverUsed();
return super.wasEverUsed();
}
}

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,24 +1,20 @@
import React, { useCallback } from 'react';
import { Keyboard, StyleSheet, TextInput, View } 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?:
| 'default'
| 'numeric'
@ -41,14 +37,12 @@ const AddressInput = ({
testID = 'AddressInput',
placeholder = loc.send.details_address,
onChangeText,
onBarScanned,
scanButtonTapped = () => {},
launchedBy,
editable = true,
inputAccessoryViewID,
onBlur = () => {},
onFocus = () => {},
onBlur = () => {},
keyboardType = 'default',
style,
}: AddressInputProps) => {
const { colors } = useTheme();
const stylesHook = StyleSheet.create({
@ -62,26 +56,8 @@ 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]}>
<View style={[styles.root, stylesHook.root, style]}>
<TextInput
testID={testID}
onChangeText={onChangeText}
@ -93,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>
);
};
@ -120,8 +88,6 @@ const styles = StyleSheet.create({
minHeight: 44,
height: 44,
alignItems: 'center',
marginVertical: 8,
marginHorizontal: 18,
borderRadius: 4,
},
input: {

View file

@ -3,31 +3,33 @@ import { Image, Keyboard, Platform, StyleSheet, Text } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
import ToolTipMenu from './TooltipMenu';
import loc from '../loc';
import { scanQrHelper } from '../helpers/scan-qr';
import { showFilePickerAndReadFile, showImagePickerAndReadImage } from '../blue_modules/fs';
import presentAlert from './Alert';
import { useTheme } from './themes';
import RNQRGenerator from 'rn-qr-generator';
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
import { useSettings } from '../hooks/context/useSettings';
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
interface AddressInputScanButtonProps {
isLoading: boolean;
launchedBy?: string;
scanButtonTapped: () => void;
onBarScanned: (ret: { data?: any }) => void;
isLoading?: boolean;
onChangeText: (text: string) => void;
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 stylesHook = StyleSheet.create({
scan: {
backgroundColor: colors.scanLabel,
@ -38,14 +40,17 @@ export const AddressInputScanButton = ({
});
const toolTipOnPress = useCallback(async () => {
await scanButtonTapped();
if (beforePress) {
await beforePress();
}
Keyboard.dismiss();
if (launchedBy) scanQrHelper(launchedBy, true).then(value => onBarScanned({ data: value }));
}, [launchedBy, onBarScanned, scanButtonTapped]);
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
}, [navigation, beforePress]);
const actions = useMemo(() => {
const availableActions = [
CommonToolTipActions.ScanQR,
CommonToolTipActions.ChoosePhoto,
CommonToolTipActions.ImportFile,
{
@ -59,18 +64,11 @@ export const AddressInputScanButton = ({
const onMenuItemPressed = useCallback(
async (action: string) => {
if (onBarScanned === undefined) throw new Error('onBarScanned is required');
switch (action) {
case CommonToolTipActions.ScanQR.id:
scanButtonTapped();
if (launchedBy) {
scanQrHelper(launchedBy)
.then(value => onBarScanned({ data: value }))
.catch(error => {
presentAlert({ message: error.message });
});
}
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
break;
case CommonToolTipActions.PasteFromClipboard.id:
try {
@ -88,8 +86,7 @@ export const AddressInputScanButton = ({
if (getImage) {
try {
const base64Data = getImage.replace(/^data:image\/jpeg;base64,/, '');
const base64Data = getImage.replace(/^data:image\/(png|jpeg|jpg);base64,/, '');
const values = await RNQRGenerator.detect({
base64: base64Data,
});
@ -135,7 +132,7 @@ export const AddressInputScanButton = ({
}
Keyboard.dismiss();
},
[launchedBy, onBarScanned, onChangeText, scanButtonTapped],
[navigation, onChangeText],
);
const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]);
@ -145,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,
@ -174,4 +179,8 @@ const styles = StyleSheet.create({
scanText: {
marginLeft: 4,
},
linkText: {
textAlign: 'center',
fontSize: 16,
},
});

View file

@ -1,6 +1,7 @@
import { Alert as RNAlert, Platform, ToastAndroid, AlertButton, AlertOptions } from 'react-native';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
import loc from '../loc';
import { navigationRef } from '../NavigationService';
export enum AlertType {
Alert,
@ -22,7 +23,7 @@ const presentAlert = (() => {
};
const showAlert = (title: string | undefined, message: string, buttons: AlertButton[], options: AlertOptions) => {
if (Platform.OS === 'ios') {
if (Platform.OS === 'ios' && navigationRef.isReady()) {
RNAlert.alert(title ?? message, title && message ? message : undefined, buttons, options);
} else {
RNAlert.alert(title ?? '', message, buttons, options);

View file

@ -3,7 +3,7 @@ import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Image, LayoutAnimation, Pressable, StyleSheet, TextInput, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native';
import { Image, LayoutAnimation, Pressable, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
import { Badge, Icon, Text } from '@rneui/themed';
import {
@ -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 => {
@ -254,11 +256,15 @@ class AmountInput extends Component {
});
return (
<TouchableWithoutFeedback
<Pressable
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}>
@ -340,7 +346,7 @@ class AmountInput extends Component {
</View>
)}
</>
</TouchableWithoutFeedback>
</Pressable>
);
}
}

View file

@ -1,10 +1,9 @@
import React, { forwardRef, useImperativeHandle, useRef, ReactElement, ComponentType, ReactNode } from 'react';
import { SheetSize, SizeInfo, TrueSheet, TrueSheetProps } from '@lodev09/react-native-true-sheet';
import { Keyboard, StyleSheet, View, TouchableOpacity, Platform, GestureResponderEvent, Text } from 'react-native';
import Ionicons from 'react-native-vector-icons/Ionicons';
import React, { forwardRef, useImperativeHandle, useRef, ReactElement, ComponentType } from 'react';
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';
import { Image } from '@rneui/base';
import { Icon } from '@rneui/base';
interface BottomModalProps extends TrueSheetProps {
children?: React.ReactNode;
@ -15,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;
@ -57,7 +56,7 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
ref,
) => {
const trueSheetRef = useRef<TrueSheet>(null);
const { colors } = useTheme();
const { colors, closeImage } = useTheme();
const stylesHook = StyleSheet.create({
barButton: {
backgroundColor: colors.lightButton,
@ -107,7 +106,12 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
testID="ModalShareButton"
key="ModalShareButton"
>
<Ionicons name="share" size={20} color={colors.buttonTextColor} />
<Icon
name={Platform.OS === 'android' ? 'share' : 'file-upload'}
type="font-awesome6"
size={20}
color={colors.buttonTextColor}
/>
</SaveFileButton>,
);
} else if (shareButtonOnPress) {
@ -118,7 +122,12 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
style={[styles.topRightButton, stylesHook.barButton]}
onPress={shareButtonOnPress}
>
<Ionicons name="share" size={20} color={colors.buttonTextColor} />
<Icon
name={Platform.OS === 'android' ? 'share' : 'file-upload'}
type="font-awesome6"
size={20}
color={colors.buttonTextColor}
/>
</TouchableOpacity>,
);
}
@ -131,11 +140,7 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
key="ModalDoneButton"
testID="ModalDoneButton"
>
{Platform.OS === 'ios' ? (
<Ionicons name="close" size={20} color={colors.buttonTextColor} />
) : (
<Image source={require('../img/close.png')} style={styles.closeButton} />
)}
<Image source={closeImage} />
</TouchableOpacity>,
);
}
@ -155,18 +160,26 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
);
}
return (
<View style={styles.headerContainer}>
<View style={styles.headerContent}>{typeof header === 'function' ? <header /> : header}</View>
{renderTopRightButton()}
</View>
);
if (showCloseButton || shareContent)
return (
<View style={styles.headerContainer}>
<View style={styles.headerContent}>{typeof header === 'function' ? <header /> : header}</View>
{renderTopRightButton()}
</View>
);
if (React.isValidElement(header)) {
return (
<View style={styles.headerContainerWithCloseButton}>
{header}
{renderTopRightButton()}
</View>
);
}
return null;
};
const renderFooter = (): ReactElement | undefined => {
// Footer is not working correctly on Android yet.
if (!footer) return undefined;
if (React.isValidElement(footer)) {
return footerDefaultMargins ? <View style={styles.footerContainer}>{footer}</View> : footer;
} else if (typeof footer === 'function') {
@ -177,7 +190,7 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
return undefined;
};
const FooterComponent = Platform.OS !== 'android' && renderFooter();
const FooterComponent = renderFooter();
return (
<TrueSheet
@ -191,7 +204,6 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
{...props}
>
<View style={styles.childrenContainer}>{children}</View>
{Platform.OS === 'android' && (renderFooter() as ReactNode)}
{renderHeader()}
</TrueSheet>
);
@ -214,6 +226,17 @@ const styles = StyleSheet.create({
right: 16,
top: 16,
},
headerContainerWithCloseButton: {
position: 'absolute',
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
minHeight: 22,
width: '100%',
top: 16,
left: 0,
justifyContent: 'space-between',
},
headerContent: {
flex: 1,
justifyContent: 'center',
@ -223,10 +246,6 @@ const styles = StyleSheet.create({
fontSize: 18,
fontWeight: 'bold',
},
closeButton: {
width: 10,
height: 10,
},
headerSubtitle: {
fontSize: 14,
},

268
components/CameraScreen.tsx Normal file
View file

@ -0,0 +1,268 @@
import React, { useState, useRef } from 'react';
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 { 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;
showImagePickerButton?: boolean;
showFilePickerButton?: boolean;
onImagePickerButtonPress?: () => void;
onFilePickerButtonPress?: () => void;
onReadCode?: (event: OnReadCodeData) => void;
}
const CameraScreen: React.FC<CameraScreenProps> = ({
onCancelButtonPress,
showImagePickerButton,
showFilePickerButton,
onImagePickerButtonPress,
onFilePickerButtonPress,
onReadCode,
}) => {
const cameraRef = useRef<CameraApi>(null);
const [torchMode, setTorchMode] = useState(false);
const [cameraType, setCameraType] = useState(CameraType.Back);
const [zoom, setZoom] = useState<number | undefined>();
const [orientationAnim] = useState(new Animated.Value(3));
const onSwitchCameraPressed = () => {
const direction = cameraType === CameraType.Back ? CameraType.Front : CameraType.Back;
setCameraType(direction);
setZoom(1); // When changing camera type, reset to default zoom for that camera
triggerSelectionHapticFeedback();
};
const onSetTorch = () => {
setTorchMode(!torchMode);
triggerSelectionHapticFeedback();
};
// Counter-rotate the icons to indicate the actual orientation of the captured photo.
// For this example, it'll behave incorrectly since UI orientation is allowed (and already-counter rotates the entire screen)
// 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, 2, 3, 4],
outputRange: ['180deg', '90deg', '0deg', '-90deg'],
});
const uiRotationStyle = rotateUi ? { transform: [{ rotate: uiRotation }] } : {};
function rotateUiTo(rotationValue: number) {
Animated.timing(orientationAnim, {
toValue: rotationValue,
useNativeDriver: true,
duration: 200,
isInteraction: false,
}).start();
}
const handleZoom = (e: { nativeEvent: { zoom: number } }) => {
console.debug('zoom', e.nativeEvent.zoom);
setZoom(e.nativeEvent.zoom);
};
const handleOrientationChange = (e: OnOrientationChangeData) => {
switch (e.nativeEvent.orientation) {
case Orientation.PORTRAIT_UPSIDE_DOWN:
console.debug('orientationChange', 'PORTRAIT_UPSIDE_DOWN');
rotateUiTo(1);
break;
case Orientation.LANDSCAPE_LEFT:
console.debug('orientationChange', 'LANDSCAPE_LEFT');
rotateUiTo(2);
break;
case Orientation.PORTRAIT:
console.debug('orientationChange', 'PORTRAIT');
rotateUiTo(3);
break;
case Orientation.LANDSCAPE_RIGHT:
console.debug('orientationChange', 'LANDSCAPE_RIGHT');
rotateUiTo(4);
break;
default:
console.debug('orientationChange', e.nativeEvent);
break;
}
};
const handleReadCode = (event: OnReadCodeData) => {
onReadCode?.(event);
};
return (
<View style={styles.screen}>
{/* 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 style={styles.cameraContainer}>
<Camera
ref={cameraRef}
style={styles.cameraPreview}
cameraType={cameraType}
scanBarcode
resizeMode="cover"
onReadCode={handleReadCode}
torchMode={torchMode ? 'on' : 'off'}
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>
{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>
);
};
export default CameraScreen;
const styles = StyleSheet.create({
activeTorch: {
backgroundColor: '#fff',
},
screen: {
height: '100%',
backgroundColor: '#000000',
},
topButtons: {
padding: 10,
zIndex: 10,
flexDirection: 'row',
justifyContent: 'space-between',
},
topButton: {
backgroundColor: '#222',
width: 44,
height: 44,
borderRadius: 22,
justifyContent: 'center',
alignItems: 'center',
},
topButtonImg: {
margin: 10,
width: 24,
height: 24,
},
cameraContainer: {
justifyContent: 'center',
flex: 1,
},
cameraPreview: {
width: '100%',
height: '100%',
},
bottomButtons: {
padding: 10,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
backTextStyle: {
padding: 10,
color: 'white',
fontSize: 20,
},
rightButtonsContainer: {
flexDirection: 'row',
alignItems: 'center',
},
bottomButton: {
backgroundColor: '#222',
width: 44,
height: 44,
borderRadius: 22,
justifyContent: 'center',
alignItems: 'center',
marginLeft: 10,
},
spacing: {
marginLeft: 20,
},
});

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,11 +1,13 @@
import React, { useCallback } from 'react';
import { View, StyleSheet, ViewStyle, TouchableOpacity } 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 loc from '../loc';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
enum ItemType {
WalletSection = 'wallet',
@ -29,11 +31,16 @@ interface ManageWalletsListItemProps {
isDraggingDisabled: boolean;
drag?: () => void;
isPlaceHolder?: boolean;
onPressIn?: () => void;
onPressOut?: () => void;
state: { wallets: TWallet[]; searchQuery: string };
navigateToWallet: (wallet: TWallet) => void;
renderHighlightedText: (text: string, query: string) => JSX.Element;
handleDeleteWallet: (wallet: TWallet) => void;
handleToggleHideBalance: (wallet: TWallet) => void;
isActive?: boolean;
style?: ViewStyle;
globalDragActive?: boolean;
}
interface SwipeContentProps {
@ -46,14 +53,21 @@ const LeftSwipeContent: React.FC<SwipeContentProps> = ({ onPress, hideBalance, c
<TouchableOpacity
onPress={onPress}
style={[styles.leftButtonContainer, { backgroundColor: colors.buttonAlternativeTextColor } as ViewStyle]}
accessibilityRole="button"
accessibilityLabel={hideBalance ? loc.transactions.details_balance_show : loc.transactions.details_balance_hide}
>
<Icon name={hideBalance ? 'eye-slash' : 'eye'} color={colors.brandingColor} type="font-awesome-5" />
</TouchableOpacity>
);
const RightSwipeContent: React.FC<Partial<SwipeContentProps>> = ({ onPress }) => (
<TouchableOpacity onPress={onPress} style={styles.rightButtonContainer as ViewStyle}>
<Icon name="delete-outline" color="#FFFFFF" />
<TouchableOpacity
onPress={onPress}
style={styles.rightButtonContainer as ViewStyle}
accessibilityRole="button"
accessibilityLabel="Delete Wallet"
>
<Icon name={Platform.OS === 'android' ? 'delete' : 'delete-outline'} color="#FFFFFF" />
</TouchableOpacity>
);
@ -67,68 +81,141 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
renderHighlightedText,
handleDeleteWallet,
handleToggleHideBalance,
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) {
setIsLoading(true);
navigateToWallet(item.data);
setIsLoading(false);
}
}, [item, navigateToWallet]);
const leftContent = useCallback(
(reset: () => void) => (
<LeftSwipeContent
onPress={() => {
handleToggleHideBalance(item.data as TWallet);
reset();
}}
hideBalance={(item.data as TWallet).hideBalance}
colors={colors}
/>
),
[colors, handleToggleHideBalance, item.data],
);
const handleLeftPress = (reset: () => void) => {
handleToggleHideBalance(item.data as TWallet);
reset();
};
const rightContent = useCallback(
(reset: () => void) => (
<RightSwipeContent
onPress={() => {
handleDeleteWallet(item.data as TWallet);
reset();
}}
/>
),
[handleDeleteWallet, item.data],
);
const leftContent = (reset: () => void) => {
resetFunctionRef.current = reset;
return <LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />;
};
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 }}
leftContent={leftContent}
rightContent={rightContent}
>
<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}
animationsEnabled={false}
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));
@ -145,25 +232,28 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
);
}
console.error('Unrecognized item type:', item);
return null;
};
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',
},
transparentBackground: {
backgroundColor: 'transparent',
},
});
export { ManageWalletsListItem, LeftSwipeContent, RightSwipeContent };
export { LeftSwipeContent, RightSwipeContent };
export default ManageWalletsListItem;

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,11 +197,15 @@ const MultipleStepsListItem = props => {
accessibilityRole="button"
disabled={props.button.disabled}
style={[styles.rowPartialRightButton, stylesHook.provideKeyButton, rightButtonOpacity]}
onPress={props.button.onPress}
onPress={onPress}
>
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText, styles.rightButton]}>
{props.button.text}
</Text>
{props.button.showActivityIndicator ? (
<ActivityIndicator />
) : (
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText, styles.rightButton]}>
{props.button.text}
</Text>
)}
</TouchableOpacity>
</View>
)}
@ -171,7 +219,11 @@ const MultipleStepsListItem = props => {
style={styles.rightButton}
onPress={props.rightButton.onPress}
>
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.rightButton.text}</Text>
{props.rightButton.showActivityIndicator ? (
<ActivityIndicator />
) : (
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.rightButton.text}</Text>
)}
</TouchableOpacity>
</View>
)}
@ -180,28 +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,
}),
rightButton: PropTypes.shape({
text: PropTypes.string,
onPress: PropTypes.func,
disabled: PropTypes.bool,
}),
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',

View file

@ -1,10 +1,11 @@
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';
import { SecondButton } from './SecondButton';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
import { useKeyboard } from '../hooks/useKeyboard';
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
@ -42,11 +43,11 @@ 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({
modalContent: {
@ -101,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 = () => {
@ -178,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);
@ -187,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
@ -256,16 +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}
scrollRef={scrollView}
isGrabberVisible={!isSuccess}
dismissible={false}
sizes={Platform.OS === 'ios' ? ['auto'] : [420, 'auto']}
footer={
!isSuccess ? (
showExplanation && modalType === MODAL_TYPES.CREATE_PASSWORD ? (
@ -278,16 +269,19 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
/>
</Animated.View>
) : (
<Animated.View style={{ opacity: fadeOutAnimation, transform: [{ scale: scaleAnimation }] }}>
<View style={styles.feeModalFooter}>
<SecondButton
title={isLoading ? '' : loc._.ok}
onPress={handleSubmit}
testID="OKButton"
loading={isLoading}
disabled={isLoading || !password || (modalType === MODAL_TYPES.CREATE_PASSWORD && !confirmPassword)}
/>
</View>
<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
@ -298,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>
)}
@ -394,32 +388,33 @@ 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: 260,
minHeight: 420,
},
feeModalFooter: {
paddingHorizontal: 16,
padding: 16,
},
feeModalFooterSpacing: {
paddingHorizontal: 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: 22,
fontSize: 20,
fontWeight: '600',
marginBottom: 16,
textAlign: 'center',
@ -432,7 +427,8 @@ const styles = StyleSheet.create({
successContainer: {
justifyContent: 'center',
alignItems: 'center',
height: 100,
margin: 24,
marginBottom: 48,
},
circle: {
width: 60,
@ -446,7 +442,4 @@ const styles = StyleSheet.create({
color: 'white',
fontSize: 30,
},
explanationScrollView: {
maxHeight: 200,
},
});

View file

@ -20,6 +20,8 @@ interface QRCodeComponentProps {
onError?: () => void;
}
const BORDER_WIDTH = 6;
const actionIcons: { [key: string]: ActionIcons } = {
Share: {
iconValue: 'square.and.arrow.up',
@ -62,7 +64,7 @@ const QRCodeComponent: React.FC<QRCodeComponentProps> = ({
onError = () => {},
}) => {
const qrCode = useRef<any>();
const { colors } = useTheme();
const { colors, dark } = useTheme();
const handleShareQRCode = () => {
qrCode.current.toDataURL((data: string) => {
@ -82,11 +84,17 @@ const QRCodeComponent: React.FC<QRCodeComponentProps> = ({
}
}, []);
// Adjust the size of the QR code to account for the border width
const newSize = dark ? size - BORDER_WIDTH * 2 : size;
const stylesHook = StyleSheet.create({
container: { borderWidth: dark ? BORDER_WIDTH : 0 },
});
const renderQRCode = (
<QRCode
value={value}
{...(isLogoRendered ? { logo: require('../img/qr-code.png') } : {})}
size={size}
size={newSize}
logoSize={logoSize}
color="#000000"
logoBackgroundColor={colors.brandingColor}
@ -99,7 +107,7 @@ const QRCodeComponent: React.FC<QRCodeComponentProps> = ({
return (
<View
style={styles.qrCodeContainer}
style={[styles.container, stylesHook.container]}
testID="BitcoinAddressQRCodeContainer"
accessibilityIgnoresInvertColors
importantForAccessibility="no-hide-descendants"
@ -120,5 +128,5 @@ const QRCodeComponent: React.FC<QRCodeComponentProps> = ({
export default QRCodeComponent;
const styles = StyleSheet.create({
qrCodeContainer: { borderWidth: 6, borderRadius: 8, borderColor: '#FFFFFF' },
container: { borderColor: '#FFFFFF' },
});

64
components/SeedWords.tsx Normal file
View file

@ -0,0 +1,64 @@
import React from 'react';
import { I18nManager, StyleSheet, Text, View } from 'react-native';
import { useTheme } from './themes';
const SeedWords = ({ seed }: { seed: string }) => {
const words = seed.split(/\s/);
const { colors } = useTheme();
const stylesHook = StyleSheet.create({
word: {
backgroundColor: colors.inputBackgroundColor,
},
wortText: {
color: colors.labelText,
},
});
return (
<View style={styles.secret}>
{words.map((secret, index) => {
const text = `${index + 1}. ${secret} `;
return (
<View style={[styles.word, stylesHook.word]} key={index}>
<Text style={[styles.wortText, stylesHook.wortText]} textBreakStrategy="simple">
{text}
</Text>
</View>
);
})}
<Text style={styles.hiddenText} testID="Secret">
{seed}
</Text>
</View>
);
};
const styles = StyleSheet.create({
word: {
marginRight: 8,
marginBottom: 8,
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
},
wortText: {
fontWeight: 'bold',
textAlign: 'left',
fontSize: 17,
},
secret: {
flexWrap: 'wrap',
justifyContent: 'center',
flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
},
hiddenText: {
height: 0,
width: 0,
},
});
export default SeedWords;

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 (
@ -340,14 +340,13 @@ const styles = StyleSheet.create({
feeModalFooter: {
paddingVertical: 46,
flexDirection: 'row',
justifyContent: 'center',
alignContent: 'center',
alignItems: 'center',
minHeight: 80,
},
feeModalFooterSpacing: {
paddingHorizontal: 24,
paddingHorizontal: 16,
},
memo: {
flexDirection: 'row',

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,50 +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 },
})) || [];
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 => {
@ -100,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);
@ -114,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
@ -179,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;
@ -164,6 +159,11 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
label: loc.transactions.expired_transaction,
icon: <TransactionExpiredIcon />,
};
} else if (!item.ispaid) {
return {
label: loc.transactions.expired_transaction,
icon: <TransactionPendingIcon />,
};
} else {
return {
label: loc.transactions.incoming_transaction,
@ -193,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);
@ -221,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,
@ -247,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]);
@ -278,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();
}
},
[
@ -285,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 {
@ -317,6 +339,8 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
};
}, [subtitleNumberOfLines]);
const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]);
return (
<ToolTipMenu
isButton
@ -338,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

@ -22,13 +22,13 @@ interface TransactionsNavigationHeaderProps {
}
const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps> = ({
wallet: initialWallet,
wallet,
onWalletUnitChange,
onManageFundsPressed,
onWalletBalanceVisibilityChange,
unit = BitcoinUnit.BTC,
}) => {
const [wallet, setWallet] = useState(initialWallet);
const { hideBalance } = wallet;
const [allowOnchainAddress, setAllowOnchainAddress] = useState(false);
const { preferredFiatCurrency } = useSettings();
@ -44,10 +44,6 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
}
}, [wallet]);
useEffect(() => {
setWallet(initialWallet);
}, [initialWallet]);
useEffect(() => {
verifyIfWalletAllowsOnchainAddress();
}, [wallet, verifyIfWalletAllowsOnchainAddress]);
@ -60,8 +56,8 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
}, [unit, wallet]);
const handleBalanceVisibility = useCallback(() => {
onWalletBalanceVisibilityChange?.(!wallet.hideBalance);
}, [onWalletBalanceVisibilityChange, wallet.hideBalance]);
onWalletBalanceVisibilityChange?.(!hideBalance);
}, [onWalletBalanceVisibilityChange, hideBalance]);
const changeWalletBalanceUnit = () => {
let newWalletPreferredUnit = wallet.getPreferredBalanceUnit();
@ -112,17 +108,17 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
];
}, []);
const balance = useMemo(() => {
const hideBalance = wallet.hideBalance;
const balanceFormatted =
unit === BitcoinUnit.LOCAL_CURRENCY
? formatBalance(wallet.getBalance(), unit, true)
: formatBalanceWithoutSuffix(wallet.getBalance(), unit, true);
return !hideBalance && balanceFormatted;
}, [unit, wallet]);
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 wallet.hideBalance
return hideBalance
? [
{
id: 'walletBalanceVisibility',
@ -148,7 +144,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
},
},
];
}, [wallet.hideBalance]);
}, [hideBalance]);
const imageSource = useMemo(() => {
switch (wallet.type) {
@ -162,7 +158,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
}, [wallet.type]);
useAnimateOnChange(balance);
useAnimateOnChange(wallet.hideBalance);
useAnimateOnChange(hideBalance);
useAnimateOnChange(unit);
useAnimateOnChange(wallet.getID?.());
@ -187,7 +183,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
actions={toolTipWalletBalanceActions}
>
<View style={styles.walletBalance}>
{wallet.hideBalance ? (
{hideBalance ? (
<BlurredBalanceView />
) : (
<View>

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,19 +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;
}
export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
({
item,
@ -186,6 +177,8 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
renderHighlightedText,
animationsEnabled = true,
isPlaceHolder = false,
onPressIn,
onPressOut,
}) => {
const scaleValue = useRef(new Animated.Value(1.0)).current;
const { colors } = useTheme();
@ -194,32 +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);
}
}, [scaleValue, animationsEnabled]);
if (onPressIn) 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);
}
}, [scaleValue, animationsEnabled]);
if (onPressOut) 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;
@ -261,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}>
@ -356,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 ? (
@ -373,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),
@ -395,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) {
@ -407,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}
@ -427,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

@ -86,6 +86,7 @@ export const BlueDarkTheme: Theme = {
customHeader: '#000000',
brandingColor: '#000000',
borderTopColor: '#9aa0aa',
background: '#000000',
foregroundColor: '#ffffff',
buttonDisabledBackgroundColor: '#3A3A3C',
buttonBackgroundColor: '#3A3A3C',

View file

@ -41,37 +41,37 @@ platform :android do
Dir.chdir(project_root) do
build_number = ENV['BUILD_NUMBER']
UI.user_error!("BUILD_NUMBER environment variable is missing") if build_number.nil?
# Extract versionName from build.gradle
version_name = sh("grep versionName android/app/build.gradle | awk '{print $2}' | tr -d '\"'").strip
UI.user_error!("Failed to extract versionName from build.gradle") if version_name.nil? || version_name.empty?
# Update versionCode in build.gradle
UI.message("Updating versionCode in build.gradle to #{build_number}...")
build_gradle_path = "android/app/build.gradle"
build_gradle_contents = File.read(build_gradle_path)
new_build_gradle_contents = build_gradle_contents.gsub(/versionCode\s+\d+/, "versionCode #{build_number}")
File.write(build_gradle_path, new_build_gradle_contents)
# Determine branch name
branch_name = ENV['GITHUB_HEAD_REF'] || `git rev-parse --abbrev-ref HEAD`.strip.gsub(/[\/\\:?*"<>|]/, '_')
# Determine branch name and sanitize it
branch_name = ENV['GITHUB_HEAD_REF'] || `git rev-parse --abbrev-ref HEAD`.strip
branch_name = branch_name.gsub(/[^a-zA-Z0-9_-]/, '_') # Replace non-alphanumeric characters with underscore
branch_name = 'master' if branch_name.nil? || branch_name.empty?
# Define APK name based on branch
signed_apk_name = branch_name != 'master' ? "BlueWallet-#{version_name}-#{build_number}-#{branch_name}.apk" : "BlueWallet-#{version_name}-#{build_number}.apk"
# Build APK
UI.message("Building APK...")
sh("cd android && ./gradlew assembleRelease")
UI.message("APK build completed.")
# Define APK name based on branch
signed_apk_name = branch_name != 'master' ?
"BlueWallet-#{version_name}-#{build_number}-#{branch_name}".gsub(/[\/\\:?*"<>|]/, '_') + ".apk" :
"BlueWallet-#{version_name}-#{build_number}.apk"
"BlueWallet-#{version_name}-#{build_number}-#{branch_name}.apk" :
"BlueWallet-#{version_name}-#{build_number}.apk"
# Define paths
unsigned_apk_path = "android/app/build/outputs/apk/release/app-release-unsigned.apk"
signed_apk_path = "android/app/build/outputs/apk/release/#{signed_apk_name}"
# Build APK
UI.message("Building APK...")
sh("cd android && ./gradlew assembleRelease --no-daemon")
UI.message("APK build completed.")
# Rename APK
if File.exist?(unsigned_apk_path)
UI.message("Renaming APK to #{signed_apk_name}...")
@ -81,14 +81,16 @@ platform :android do
UI.error("Unsigned APK not found at path: #{unsigned_apk_path}")
next
end
# Sign APK
UI.message("Signing APK with apksigner...")
apksigner_path = "#{ENV['ANDROID_HOME']}/build-tools/34.0.0/apksigner"
apksigner_path = Dir.glob("#{ENV['ANDROID_HOME']}/build-tools/*/apksigner").sort.last
UI.user_error!("apksigner not found in Android build-tools") if apksigner_path.nil? || apksigner_path.empty?
sh("#{apksigner_path} sign --ks #{project_root}/bluewallet-release-key.keystore --ks-pass=pass:#{ENV['KEYSTORE_PASSWORD']} #{signed_apk_path}")
UI.message("APK signed successfully: #{signed_apk_path}")
end
end
end
desc "Upload APK to BrowserStack and post result as PR comment"
lane :upload_to_browserstack_and_comment do
@ -126,16 +128,18 @@ platform :android do
You can test it on the following devices:
- [Google Pixel 5 (Android 12.0)](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 7 (Android 13.0)](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 8 (Android 14.0)](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 3a (Android 9.0)](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 5 (Android 13.0)](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 Z Fold 6 (Android 14.0)](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 Tab S9 (Android 13.0)](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&browser=chrome)
**Filename**: [#{apk_filename}](#{apk_download_url})
**BrowserStack App URL**: #{app_url}
COMMENT
@ -188,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
@ -234,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"
@ -398,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
@ -589,5 +700,4 @@ lane :update_release_notes do |options|
UI.error("No localization found for locale #{locale}")
end
end
end
end

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,54 +1,6 @@
import { Platform } from 'react-native';
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { navigationRef } from '../NavigationService';
/**
* Helper function that navigates to ScanQR screen, and returns promise that will resolve with the result of a scan,
* and then navigates back. If QRCode scan was closed, promise resolves to null.
*
* @param currentScreenName {string}
* @param showFileImportButton {boolean}
*
* @param onDismiss {function} - if camera is closed via X button it gets triggered
* @param useMerge {boolean} - if true, will merge the new screen with the current screen, otherwise will replace the current screen
* @return {Promise<string>}
*/
function scanQrHelper(
currentScreenName: string,
showFileImportButton = true,
onDismiss?: () => void,
useMerge = true,
): Promise<string | null> {
return requestCameraAuthorization().then(() => {
return new Promise(resolve => {
let params = {};
if (useMerge) {
const onBarScanned = function (data: any) {
setTimeout(() => resolve(data.data || data), 1);
navigationRef.navigate({ name: currentScreenName, params: data, merge: true });
};
params = {
showFileImportButton: Boolean(showFileImportButton),
onDismiss,
onBarScanned,
};
} else {
params = { launchedBy: currentScreenName, showFileImportButton: Boolean(showFileImportButton) };
}
navigationRef.navigate({
name: 'ScanQRCodeRoot',
params: {
screen: 'ScanQRCode',
params,
},
merge: true,
});
});
});
}
import { navigationRef } from '../NavigationService.ts';
const isCameraAuthorizationStatusGranted = async () => {
const status = await check(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA);
@ -59,4 +11,17 @@ const requestCameraAuthorization = () => {
return request(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA);
};
export { scanQrHelper, 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,45 +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 {
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 {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: 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);
}
} catch (error) {
console.error('Error detecting QR code:', error);
}
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), {
@ -214,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;
@ -242,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();
@ -287,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);
@ -298,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

@ -3,9 +3,16 @@ import { navigationRef } from '../NavigationService';
import { presentWalletExportReminder } from '../helpers/presentWalletExportReminder';
import { unlockWithBiometrics, useBiometrics } from './useBiometrics';
import { useStorage } from './context/useStorage';
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'];
@ -15,96 +22,125 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
const { wallets, saveToDisk } = useStorage();
const { isBiometricUseEnabled } = useBiometrics();
const enhancedNavigate: NavigationProp<ParamListBase>['navigate'] = (
screenOrOptions: any,
params?: any,
options?: { merge?: boolean },
) => {
let screenName: string;
if (typeof screenOrOptions === 'string') {
screenName = screenOrOptions;
} else if (typeof screenOrOptions === 'object' && 'name' in screenOrOptions) {
screenName = screenOrOptions.name;
params = screenOrOptions.params; // Assign params from object if present
} else {
throw new Error('Invalid navigation options');
}
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;
const isRequiresBiometrics = requiresBiometrics.includes(screenName);
const isRequiresWalletExportIsSaved = requiresWalletExportIsSaved.includes(screenName);
const proceedWithNavigation = () => {
console.log('Proceeding with navigation to', screenName);
if (navigationRef.current?.isReady()) {
if (typeof screenOrOptions === 'string') {
originalNavigation.navigate({ name: screenOrOptions, params, merge: options?.merge });
} else {
originalNavigation.navigate({ ...screenOrOptions, params, merge: options?.merge });
}
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;
} else if (typeof screenOrOptions === 'object' && 'name' in screenOrOptions) {
screenName = screenOrOptions.name;
params = screenOrOptions.params; // Assign params from object if present
} else {
throw new Error('Invalid navigation options');
}
};
(async () => {
if (isRequiresBiometrics) {
const isBiometricsEnabled = await isBiometricUseEnabled();
if (isBiometricsEnabled) {
const isAuthenticated = await unlockWithBiometrics();
if (isAuthenticated) {
proceedWithNavigation();
return;
const isRequiresBiometrics = requiresBiometrics.includes(screenName);
const isRequiresWalletExportIsSaved = requiresWalletExportIsSaved.includes(screenName);
const proceedWithNavigation = () => {
console.log('Proceeding with navigation to', screenName);
if (navigationRef.current?.isReady()) {
if (typeof screenOrOptions === 'string') {
originalNavigation.navigate({ name: screenOrOptions, params, merge: options?.merge });
} 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
originalNavigation.navigate({ ...screenOrOptions, params, merge: options?.merge });
}
}
}
if (isRequiresWalletExportIsSaved) {
console.log('Checking if wallet export is saved');
let walletID: string | undefined;
if (params && params.walletID) {
walletID = params.walletID;
} else if (params && params.params && params.params.walletID) {
walletID = params.params.walletID;
}
if (!walletID) {
};
(async () => {
// NEW: If the current (active) screen is 'ScanQRCode', bypass all checks.
const currentRouteName = navigationRef.current?.getCurrentRoute()?.name;
if (currentRouteName === 'ScanQRCode') {
proceedWithNavigation();
return;
}
const wallet = wallets.find(w => w.getID() === walletID);
if (wallet && !wallet.getUserHasSavedExport()) {
try {
await presentWalletExportReminder();
wallet.setUserHasSavedExport(true);
await saveToDisk(); // Assuming saveToDisk() returns a Promise.
if (isRequiresBiometrics) {
const isBiometricsEnabled = await isBiometricUseEnabled();
if (isBiometricsEnabled) {
const isAuthenticated = await unlockWithBiometrics();
if (isAuthenticated) {
proceedWithNavigation();
return;
} else {
console.error('Biometric authentication failed');
// Do not proceed if authentication fails.
return;
}
}
}
if (isRequiresWalletExportIsSaved) {
console.log('Checking if wallet export is saved');
let walletID: string | undefined;
if (params && params.walletID) {
walletID = params.walletID;
} else if (params && params.params && params.params.walletID) {
walletID = params.params.walletID;
}
if (!walletID) {
proceedWithNavigation();
} catch (error) {
if (error) {
return;
}
const wallet = wallets.find(w => w.getID() === walletID);
if (wallet && !wallet.getUserHasSavedExport()) {
try {
await presentWalletExportReminder();
wallet.setUserHasSavedExport(true);
await saveToDisk();
proceedWithNavigation();
} catch (error) {
// If there was an error (or the user cancelled), navigate to the wallet export screen.
originalNavigation.navigate('WalletExportRoot', {
screen: 'WalletExport',
params: { walletID },
});
}
return; // Do not proceed with the original navigation if reminder was shown.
}
return; // Prevent proceeding with the original navigation if the reminder is shown
}
}
proceedWithNavigation();
})();
};
const navigateToWalletsList = () => {
// If the target screen is ScanQRCode, request camera authorization.
if (screenName === 'ScanQRCode') {
await requestCameraAuthorization();
}
proceedWithNavigation();
})();
},
[originalNavigation, isBiometricUseEnabled, wallets, saveToDisk],
);
const navigateToWalletsList = useCallback(() => {
enhancedNavigate('WalletsList');
}
}, [enhancedNavigate]);
return {
...originalNavigation,
navigate: enhancedNavigate,
navigateToWalletsList,
};
return useMemo(
() => ({
...originalNavigation,
navigate: enhancedNavigate,
navigateToWalletsList,
}),
[originalNavigation, enhancedNavigate, navigateToWalletsList],
);
};
// 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
};
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

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 = 60;
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,7 +993,7 @@
};
};
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "BlueWallet" */;
compatibilityVersion = "Xcode 15.0";
compatibilityVersion = "Xcode 15.3";
developmentRegion = en_US;
hasScannedForEncodings = 0;
knownRegions = (
@ -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 = 1703136799;
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 = 13.4;
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.6;
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 = 1703136799;
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 = 13.4;
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.6;
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 = 1703136799;
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.6;
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 = 1703136799;
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.6;
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 = 1703136799;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
@ -1670,7 +1672,7 @@
"DEVELOPMENT_TEAM[sdk=macosx*]" = A7W54YZ4WU;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = Widgets/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1681,7 +1683,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.0.6;
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 = 1703136799;
CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
@ -1726,7 +1728,7 @@
"DEVELOPMENT_TEAM[sdk=macosx*]" = A7W54YZ4WU;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = Widgets/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1737,7 +1739,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)",
);
MARKETING_VERSION = 7.0.6;
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 = 1703136799;
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.6;
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 = 1703136799;
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.6;
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 = 1703136799;
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.6;
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 = 1703136799;
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.6;
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 {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View file

@ -31,61 +31,61 @@
"size" : "1024x1024"
},
{
"filename" : "16pt@1x.png",
"filename" : "icon_16x16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "16pt@2x.png",
"filename" : "icon_16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "32pt@1x.png",
"filename" : "icon_32x32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "32pt@2x.png",
"filename" : "icon_32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "128pt@1x.png",
"filename" : "icon_128x128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "128pt@2x.png",
"filename" : "icon_128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "256pt@1x.png",
"filename" : "icon_256x256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "256pt@2x.png",
"filename" : "icon_256x256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "512pt@1x.png",
"filename" : "icon_512x512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "512pt@2x.png",
"filename" : "icon_512x512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

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