Compare commits

..

758 commits

Author SHA1 Message Date
GLaDOS
c7909049dc
Merge pull request #7694 from BlueWallet/handled
FIX: Dleeting wallet would not popToTop after deleting
2025-03-13 13:44:07 +00:00
Marcos Rodriguez Velez
b5270d0a07 FIX: Dleeting wallet would not popToTop after deleting 2025-03-13 08:10:06 -04:00
GLaDOS
4f3b828990
Merge pull request #7689 from BlueWallet/sle
ADD: Allow quick tap to copy
2025-03-13 11:04:17 +00:00
GLaDOS
26720e8284
Merge pull request #7692 from BlueWallet/f
FIX: Issue - Function to broadcast transaction from *.final PSBT not …
2025-03-13 10:46:24 +00:00
Marcos Rodriguez Velez
a80bacc0f4 FIX: Issue - Function to broadcast transaction from *.final PSBT not available 7.1.4 #7688 2025-03-12 22:13:09 -04:00
Marcos Rodriguez Velez
5f18540ca7 FIX: UX in ManageWallets 2025-03-12 20:42:29 -04:00
Marcos Rodriguez Velez
c14cb3508c ADD: Allow quick tap to copy 2025-03-12 19:00:08 -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 14053 additions and 8987 deletions

View file

@ -47,6 +47,24 @@
"device": "emulator", "device": "emulator",
"app": "android.debug" "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": { "android.release": {
"device": "emulator", "device": "emulator",
"app": "android.release" "app": "android.release"

View file

@ -22,12 +22,40 @@ jobs:
branch_name: ${{ steps.get_latest_commit_details.outputs.branch_name }} branch_name: ${{ steps.get_latest_commit_details.outputs.branch_name }}
env: env:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
MATCH_READONLY: "true"
steps: steps:
- name: Checkout Project - name: Checkout Project
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 # Ensures the full Git history is available 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 - name: Ensure Correct Branch
if: github.ref != 'refs/heads/master' if: github.ref != 'refs/heads/master'
@ -67,15 +95,32 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: 'npm'
- uses: maxim-lobanov/setup-xcode@v1 - uses: maxim-lobanov/setup-xcode@v1
with: 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 - name: Set Up Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.1.6 ruby-version: 3.1.6
bundler-cache: true
- name: Install Dependencies with Bundler - name: Install Dependencies with Bundler
run: | run: |
@ -88,6 +133,7 @@ jobs:
- name: Install CocoaPods Dependencies - name: Install CocoaPods Dependencies
run: | run: |
bundle exec fastlane ios install_pods bundle exec fastlane ios install_pods
echo "CocoaPods dependencies installed successfully"
- name: Generate Build Number Based on Timestamp - name: Generate Build Number Based on Timestamp
id: generate_build_number id: generate_build_number
@ -133,8 +179,26 @@ jobs:
- name: Build App - name: Build App
id: build_app id: build_app
run: | run: |
bundle exec fastlane ios build_app_lane --verbose bundle exec fastlane ios build_app_lane
echo "ipa_output_path=$IPA_OUTPUT_PATH" >> $GITHUB_OUTPUT # Set the IPA output path for future jobs
# 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 - name: Upload Bugsnag Sourcemaps
if: success() if: success()
@ -142,8 +206,8 @@ jobs:
env: env:
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }} BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
BUGSNAG_RELEASE_STAGE: production BUGSNAG_RELEASE_STAGE: production
PROJECT_VERSION: ${{ needs.build.outputs.project_version }} PROJECT_VERSION: ${{ env.PROJECT_VERSION }}
NEW_BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }} NEW_BUILD_NUMBER: ${{ env.NEW_BUILD_NUMBER }}
- name: Upload Build Logs - name: Upload Build Logs
if: always() if: always()
@ -151,13 +215,32 @@ jobs:
with: with:
name: build_logs name: build_logs
path: ./ios/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 - name: Upload IPA as Artifact
if: success() if: success()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: BlueWallet_${{env.PROJECT_VERSION}}_${{env.NEW_BUILD_NUMBER}}.ipa name: BlueWallet_IPA
path: ${{ env.IPA_OUTPUT_PATH }} # Directly from Fastfile `IPA_OUTPUT_PATH` path: ${{ env.IPA_OUTPUT_PATH }}
retention-days: 7
- name: Delete Temporary Keychain
if: always()
run: bundle exec fastlane ios delete_temp_keychain
testflight-upload: testflight-upload:
needs: build needs: build
@ -177,6 +260,7 @@ jobs:
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.1.6 ruby-version: 3.1.6
bundler-cache: true
- name: Install Dependencies with Bundler - name: Install Dependencies with Bundler
run: | run: |
@ -186,18 +270,11 @@ jobs:
- name: Download IPA from Artifact - name: Download IPA from Artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: BlueWallet_${{ needs.build.outputs.project_version }}_${{ needs.build.outputs.new_build_number }}.ipa name: BlueWallet_IPA
path: ./ path: ./
- name: Create App Store Connect API Key JSON - name: Create App Store Connect API Key JSON
run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' > ./appstore_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 - 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 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 - name: Verify IPA Path Before Upload
run: | run: |
if [ ! -f "$IPA_OUTPUT_PATH" ]; then 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 exit 1
else
echo "âś… Found IPA at: $IPA_OUTPUT_PATH"
fi fi
- name: Print Environment Variables for Debugging - name: Print Environment Variables for Debugging
run: | run: |
echo "LATEST_COMMIT_MESSAGE: $LATEST_COMMIT_MESSAGE" echo "LATEST_COMMIT_MESSAGE: $LATEST_COMMIT_MESSAGE"
echo "BRANCH_NAME: $BRANCH_NAME" 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 - name: Upload to TestFlight
run: | run: bundle exec fastlane ios upload_to_testflight_lane
ls -la $IPA_OUTPUT_PATH
bundle exec fastlane ios upload_to_testflight_lane
env: env:
APP_STORE_CONNECT_API_KEY_PATH: $(pwd)/appstore_api_key.p8 APP_STORE_CONNECT_API_KEY_PATH: $(pwd)/appstore_api_key.p8
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} 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_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 }} APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
IPA_OUTPUT_PATH: ${{ env.IPA_OUTPUT_PATH }}
- name: Post PR Comment - name: Post PR Comment
if: success() && github.event_name == 'pull_request' if: success() && github.event_name == 'pull_request'
uses: actions/github-script@v6 uses: actions/github-script@v6
env: env:
BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }} BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }}
PROJECT_VERSION: ${{ needs.build.outputs.project_version }}
LATEST_COMMIT_MESSAGE: ${{ needs.build.outputs.latest_commit_message }} LATEST_COMMIT_MESSAGE: ${{ needs.build.outputs.latest_commit_message }}
with: with:
script: | script: |
const buildNumber = process.env.BUILD_NUMBER; 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 prNumber = context.payload.pull_request.number;
const repo = context.repo; const repo = context.repo;
github.rest.issues.createComment({ github.rest.issues.createComment({

View file

@ -4,7 +4,7 @@ on:
pull_request: pull_request:
branches: branches:
- master - master
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened, labeled, unlabeled]
push: push:
branches: branches:
- master - master
@ -97,11 +97,12 @@ jobs:
with: with:
name: signed-apk name: signed-apk
path: ${{ env.APK_PATH }} path: ${{ env.APK_PATH }}
if-no-files-found: error
browserstack: browserstack:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: buildReleaseApk needs: buildReleaseApk
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'browserstack') }}
steps: steps:
- name: Checkout code - 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 { NavigationContainer } from '@react-navigation/native';
import React from 'react'; import React from 'react';
import { useColorScheme } from 'react-native'; import { useColorScheme } from 'react-native';
@ -9,23 +7,26 @@ import { SettingsProvider } from './components/Context/SettingsProvider';
import { BlueDarkTheme, BlueDefaultTheme } from './components/themes'; import { BlueDarkTheme, BlueDefaultTheme } from './components/themes';
import MasterView from './navigation/MasterView'; import MasterView from './navigation/MasterView';
import { navigationRef } from './NavigationService'; import { navigationRef } from './NavigationService';
import { useLogger } from '@react-navigation/devtools';
import { StorageProvider } from './components/Context/StorageProvider'; import { StorageProvider } from './components/Context/StorageProvider';
const App = () => { const App = () => {
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
useLogger(navigationRef);
return ( return (
<LargeScreenProvider> <NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}>
<NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}> <SafeAreaProvider>
<SafeAreaProvider> <LargeScreenProvider>
<StorageProvider> <StorageProvider>
<SettingsProvider> <SettingsProvider>
<MasterView /> <MasterView />
</SettingsProvider> </SettingsProvider>
</StorageProvider> </StorageProvider>
</SafeAreaProvider> </LargeScreenProvider>
</NavigationContainer> </SafeAreaProvider>
</LargeScreenProvider> </NavigationContainer>
); );
}; };

View file

@ -40,9 +40,16 @@ export const BlueCard = props => {
return <View {...props} style={{ padding: 20 }} />; return <View {...props} style={{ padding: 20 }} />;
}; };
export const BlueText = props => { export const BlueText = ({ bold = false, ...props }) => {
const { colors } = useTheme(); 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} />; return <Text {...props} style={style} />;
}; };
@ -75,6 +82,7 @@ export const BlueFormMultiInput = props => {
multiline multiline
underlineColorAndroid="transparent" underlineColorAndroid="transparent"
numberOfLines={4} numberOfLines={4}
editable={!props.editable}
style={{ style={{
paddingHorizontal: 8, paddingHorizontal: 8,
paddingVertical: 16, 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 # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby "3.1.6" ruby "3.1.6"
gem 'rubyzip', '2.3.2' gem 'rubyzip', '2.4.1'
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' gem 'cocoapods', '~> 1.14.3'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' gem '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') plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path) eval_gemfile(plugins_path) if File.exist?(plugins_path)

View file

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

View file

@ -73,6 +73,10 @@ def enableProguardInReleaseBuilds = false
def jscFlavor = 'org.webkit:android-jsc-intl:+' def jscFlavor = 'org.webkit:android-jsc-intl:+'
android { android {
androidResources {
noCompress += ["bundle"]
}
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion buildToolsVersion rootProject.ext.buildToolsVersion
compileSdkVersion rootProject.ext.compileSdkVersion compileSdkVersion rootProject.ext.compileSdkVersion
@ -83,7 +87,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1 versionCode 1
versionName "7.0.6" versionName "7.1.5"
testBuildType System.getProperty('testBuildType', 'debug') testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
} }
@ -133,7 +137,7 @@ dependencies {
androidTestImplementation('com.wix:detox:0.1.1') androidTestImplementation('com.wix:detox:0.1.1')
implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.appcompat:appcompat:1.7.0'
implementation fileTree(dir: "libs", include: ["*.jar"]) 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.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.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" /> <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 <application
android:name=".MainApplication" android:name=".MainApplication"
@ -58,7 +51,15 @@
<meta-data <meta-data
android:name="firebase_analytics_collection_enabled" android:name="firebase_analytics_collection_enabled"
android:value="false" /> 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.RNPushNotificationActions" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" /> <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
<receiver <receiver
@ -106,14 +107,12 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <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" /> <data android:scheme="file" android:mimeType="application/octet-stream" android:pathPattern=".*\\.psbt" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <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/jpeg" />
<data android:scheme="file" android:mimeType="image/png" /> <data android:scheme="file" android:mimeType="image/png" />
<data android:scheme="file" android:mimeType="image/jpg" /> <data android:scheme="file" android:mimeType="image/jpg" />
@ -123,7 +122,6 @@
<intent-filter tools:ignore="AppLinkUrlError"> <intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bitcoin" /> <data android:scheme="bitcoin" />
<data android:scheme="lightning" /> <data android:scheme="lightning" />
<data android:scheme="bluewallet" /> <data android:scheme="bluewallet" />

View file

@ -125,6 +125,13 @@
"symbol": "ÂŁ", "symbol": "ÂŁ",
"country": "United Kingdom (British Pound)" "country": "United Kingdom (British Pound)"
}, },
"HKD": {
"endPointKey": "HKD",
"locale": "zh-HK",
"source": "CoinGecko",
"symbol": "HK$",
"country": "Hong Kong (Hong Kong Dollar)"
},
"HRK": { "HRK": {
"endPointKey": "HRK", "endPointKey": "HRK",
"locale": "hr-HR", "locale": "hr-HR",
@ -286,6 +293,13 @@
"symbol": "zł", "symbol": "zł",
"country": "Poland (Polish Zloty)" "country": "Poland (Polish Zloty)"
}, },
"PYG": {
"endPointKey": "PYG",
"locale": "es-PY",
"source": "CoinDesk",
"symbol": "₲",
"country": "Paraguay (Paraguayan Guarani)"
},
"QAR": { "QAR": {
"endPointKey": "QAR", "endPointKey": "QAR",
"locale": "ar-QA", "locale": "ar-QA",
@ -303,7 +317,7 @@
"RSD": { "RSD": {
"endPointKey": "RSD", "endPointKey": "RSD",
"locale": "sr-RS", "locale": "sr-RS",
"source": "CoinGecko", "source": "CoinDesk",
"symbol": "DIN", "symbol": "DIN",
"country": "Serbia (Serbian Dinar)" "country": "Serbia (Serbian Dinar)"
}, },

View file

@ -4,32 +4,46 @@ import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider import android.appwidget.AppWidgetProvider
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.work.WorkManager import androidx.work.WorkManager
class BitcoinPriceWidget : AppWidgetProvider() { 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) { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds) super.onUpdate(context, appWidgetManager, appWidgetIds)
Log.d("BitcoinPriceWidget", "onUpdate called") for (widgetId in appWidgetIds) {
WidgetUpdateWorker.scheduleWork(context) Log.d(TAG, "Updating widget with ID: $widgetId")
WidgetUpdateWorker.scheduleWork(context)
}
} }
override fun onEnabled(context: Context) { override fun onEnabled(context: Context) {
super.onEnabled(context) super.onEnabled(context)
Log.d("BitcoinPriceWidget", "onEnabled called") Log.d(TAG, "onEnabled called")
WidgetUpdateWorker.scheduleWork(context) WidgetUpdateWorker.scheduleWork(context)
} }
override fun onDisabled(context: Context) { override fun onDisabled(context: Context) {
super.onDisabled(context) super.onDisabled(context)
Log.d("BitcoinPriceWidget", "onDisabled called") Log.d(TAG, "onDisabled called")
clearCache(context) clearCache(context)
WorkManager.getInstance(context).cancelUniqueWork(WidgetUpdateWorker.WORK_NAME) WorkManager.getInstance(context).cancelUniqueWork(WidgetUpdateWorker.WORK_NAME)
} }
private fun clearCache(context: Context) { override fun onDeleted(context: Context, appWidgetIds: IntArray) {
val sharedPref = context.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE) super.onDeleted(context, appWidgetIds)
sharedPref.edit().clear().apply() // Clear all preferences in the group Log.d(TAG, "onDeleted called for widgets: ${appWidgetIds.joinToString()}")
Log.d("BitcoinPriceWidget", "Cache cleared from group.io.bluewallet.bluewallet")
} }
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.app.Application
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import com.bugsnag.android.Bugsnag import com.bugsnag.android.Bugsnag
import com.facebook.react.PackageList import com.facebook.react.PackageList
import com.facebook.react.ReactApplication 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.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader import com.facebook.soloader.SoLoader
import com.facebook.react.modules.i18nmanager.I18nUtil import com.facebook.react.modules.i18nmanager.I18nUtil
class MainApplication : Application(), ReactApplication { 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 = override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) { object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> = override fun getPackages(): List<ReactPackage> =
@ -35,27 +45,29 @@ class MainApplication : Application(), ReactApplication {
override val reactHost: ReactHost override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost) get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
sharedPref = getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
sharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
val sharedI18nUtilInstance = I18nUtil.getInstance() val sharedI18nUtilInstance = I18nUtil.getInstance()
sharedI18nUtilInstance.allowRTL(applicationContext, true) sharedI18nUtilInstance.allowRTL(applicationContext, true)
SoLoader.init(this, /* native exopackage */ false) SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app. // If you opted-in for the New Architecture, we load the native entry point for this app.
load() 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") val isDoNotTrackEnabled = sharedPref.getString("donottrack", "0")
// Check if do not track is not enabled and initialize Bugsnag if so
if (isDoNotTrackEnabled != "1") { if (isDoNotTrackEnabled != "1") {
// Initialize Bugsnag or your error tracking here
Bugsnag.start(this) Bugsnag.start(this)
} }
} }

View file

@ -2,20 +2,21 @@ package io.bluewallet.bluewallet
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject import org.json.JSONObject
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
object MarketAPI { object MarketAPI {
private const val TAG = "MarketAPI" private const val TAG = "MarketAPI"
private val client = OkHttpClient()
var baseUrl: String? = null var baseUrl: String? = null
fun fetchPrice(context: Context, currency: String): String? { suspend fun fetchPrice(context: Context, currency: String): String? {
return try { return try {
// Load the JSON data from the assets
val fiatUnitsJson = context.assets.open("fiatUnits.json").bufferedReader().use { it.readText() } val fiatUnitsJson = context.assets.open("fiatUnits.json").bufferedReader().use { it.readText() }
val json = JSONObject(fiatUnitsJson) val json = JSONObject(fiatUnitsJson)
val currencyInfo = json.getJSONObject(currency) val currencyInfo = json.getJSONObject(currency)
@ -25,26 +26,16 @@ object MarketAPI {
val urlString = buildURLString(source, endPointKey) val urlString = buildURLString(source, endPointKey)
Log.d(TAG, "Fetching price from URL: $urlString") Log.d(TAG, "Fetching price from URL: $urlString")
val url = URL(urlString) val request = Request.Builder().url(urlString).build()
val urlConnection = url.openConnection() as HttpURLConnection val response = withContext(Dispatchers.IO) { client.newCall(request).execute() }
urlConnection.requestMethod = "GET"
urlConnection.connect()
val responseCode = urlConnection.responseCode if (!response.isSuccessful) {
if (responseCode != 200) { Log.e(TAG, "Failed to fetch price. Response code: ${response.code}")
Log.e(TAG, "Failed to fetch price. Response code: $responseCode")
return null return null
} }
val reader = InputStreamReader(urlConnection.inputStream) val jsonResponse = response.body?.string() ?: return null
val jsonResponse = StringBuilder() parseJSONBasedOnSource(jsonResponse, source, endPointKey)
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)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error fetching price", e) Log.e(TAG, "Error fetching price", e)
null null
@ -65,7 +56,8 @@ object MarketAPI {
"CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.lowercase()}" "CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.lowercase()}"
"BNR" -> "https://www.bnr.ro/nbrfxrates.xml" "BNR" -> "https://www.bnr.ro/nbrfxrates.xml"
"Kraken" -> "https://api.kraken.com/0/public/Ticker?pair=XXBTZ${endPointKey.uppercase()}" "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") "coinpaprika" -> json.getJSONObject("quotes").getJSONObject("INR").getString("price")
"Coinbase" -> json.getJSONObject("data").getString("amount") "Coinbase" -> json.getJSONObject("data").getString("amount")
"Kraken" -> json.getJSONObject("result").getJSONObject("XXBTZ${endPointKey.uppercase()}").getJSONArray("c").getString(0) "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 else -> null
} }
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -1,8 +1,10 @@
package io.bluewallet.bluewallet package io.bluewallet.bluewallet
import android.app.PendingIntent
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log import android.util.Log
import android.view.View import android.view.View
@ -13,8 +15,10 @@ import java.text.NumberFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit 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 { companion object {
const val TAG = "WidgetUpdateWorker" const val TAG = "WidgetUpdateWorker"
@ -35,66 +39,57 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
} }
private lateinit var sharedPref: SharedPreferences 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") Log.d(TAG, "Widget update worker running")
sharedPref = applicationContext.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE) sharedPref = applicationContext.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
registerPreferenceChangeListener()
val appWidgetManager = AppWidgetManager.getInstance(applicationContext) val appWidgetManager = AppWidgetManager.getInstance(applicationContext)
val thisWidget = ComponentName(applicationContext, BitcoinPriceWidget::class.java) val thisWidget = ComponentName(applicationContext, BitcoinPriceWidget::class.java)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget) val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
val views = RemoteViews(applicationContext.packageName, R.layout.widget_layout) 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 preferredCurrency = sharedPref.getString("preferredCurrency", null) ?: "USD"
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", null) ?: "en-US" val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", null) ?: "en-US"
val previousPrice = sharedPref.getString("previous_price", null) val previousPrice = sharedPref.getString("previous_price", null)
val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date()) val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
fetchPrice(preferredCurrency) { fetchedPrice, error -> val fetchedPrice = fetchPrice(preferredCurrency)
handlePriceResult(
appWidgetManager, appWidgetIds, views, sharedPref, handlePriceResult(
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale, error appWidgetManager, appWidgetIds, views, sharedPref,
) fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
} )
return Result.success() return Result.success()
} }
private fun registerPreferenceChangeListener() { private suspend fun fetchPrice(currency: String?): String? {
preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key -> return withContext(Dispatchers.IO) {
if (key == "preferredCurrency" || key == "preferredCurrencyLocale" || key == "previous_price") { MarketAPI.fetchPrice(applicationContext, currency ?: "USD")
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
)
} }
} }
@ -107,24 +102,27 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
previousPrice: String?, previousPrice: String?,
currentTime: String, currentTime: String,
preferredCurrency: String?, preferredCurrency: String?,
preferredCurrencyLocale: String?, preferredCurrencyLocale: String?
error: String?
) { ) {
val isPriceFetched = fetchedPrice != null val isPriceFetched = fetchedPrice != null
val isPriceCached = previousPrice != null val isPriceCached = previousPrice != null
if (error != null || !isPriceFetched) { if (!isPriceFetched) {
Log.e(TAG, "Error fetching price: $error") Log.e(TAG, "Error fetching price.")
if (!isPriceCached) { if (!isPriceCached) {
showLoadingError(views) showLoadingError(views)
} else { } else {
displayCachedPrice(views, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale) displayCachedPrice(views, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale)
} }
} else { } else {
displayFetchedPrice( if (fetchedPrice != null) {
views, fetchedPrice!!, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale displayFetchedPrice(
) views, fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
savePrice(sharedPref, fetchedPrice) )
}
if (fetchedPrice != null) {
savePrice(sharedPref, fetchedPrice)
}
} }
appWidgetManager.updateAppWidget(appWidgetIds, views) appWidgetManager.updateAppWidget(appWidgetIds, views)
@ -132,7 +130,7 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
private fun showLoadingError(views: RemoteViews) { private fun showLoadingError(views: RemoteViews) {
views.apply { 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.price_value, View.GONE)
setViewVisibility(R.id.last_updated_label, View.GONE) setViewVisibility(R.id.last_updated_label, View.GONE)
setViewVisibility(R.id.last_updated_time, View.GONE) setViewVisibility(R.id.last_updated_time, View.GONE)
@ -216,15 +214,6 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
return currencyFormat 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) { private fun savePrice(sharedPref: SharedPreferences, price: String) {
sharedPref.edit().putString("previous_price", price).apply() sharedPref.edit().putString("previous_price", price).apply()
} }

View file

@ -13,13 +13,14 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:visibility="gone"/> android:visibility="visible"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:gravity="end"> android:gravity="end"
android:layout_gravity="end">
<TextView <TextView
android:id="@+id/last_updated_label" android:id="@+id/last_updated_label"
style="@style/WidgetTextSecondary" style="@style/WidgetTextSecondary"
@ -29,69 +30,86 @@
android:textSize="12sp" android:textSize="12sp"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:visibility="gone"/> android:visibility="gone"
android:layout_gravity="end"/>
<TextView <TextView
android:id="@+id/last_updated_time" android:id="@+id/last_updated_time"
style="@style/WidgetTextPrimary" style="@style/WidgetTextPrimary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="" android:text="--:--"
android:textSize="12sp" android:textSize="12sp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:visibility="gone"/> android:visibility="gone"
android:layout_gravity="end"/>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:gravity="end"> android:gravity="end"
android:layout_gravity="end">
<TextView <TextView
android:id="@+id/price_value" android:id="@+id/price_value"
style="@style/WidgetTextPrimary" style="@style/WidgetTextPrimary"
android:layout_width="wrap_content" android:layout_width="match_parent"
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_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="4dp" android:layout_gravity="end"
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_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"/> android:layout_marginBottom="8dp"
<TextView android:autoSizeMaxTextSize="24sp"
android:id="@+id/previous_price" android:autoSizeMinTextSize="12sp"
style="@style/WidgetTextPrimary" 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_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="" android:orientation="horizontal"
android:textSize="12sp" android:gravity="end"
android:layout_marginEnd="8dp" android:visibility="gone"
android:layout_marginBottom="8dp"/> android:layout_gravity="end">
</LinearLayout></LinearLayout>
<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> </LinearLayout>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_layout" android:initialLayout="@layout/widget_layout"
android:minWidth="160dp" android:minWidth="170dp"
android:minHeight="100dp" android:minHeight="100dp"
android:updatePeriodMillis="0" android:updatePeriodMillis="0"
android:widgetCategory="home_screen" 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 { buildscript {
ext { ext {
minSdkVersion = 24 minSdkVersion = 24
buildToolsVersion = "34.0.0" buildToolsVersion = "35.0.0"
compileSdkVersion = 34 compileSdkVersion = 35
targetSdkVersion = 34 targetSdkVersion = 34
googlePlayServicesVersion = "16.+" googlePlayServicesVersion = "16.+"
googlePlayServicesIidVersion = "16.0.1" googlePlayServicesIidVersion = "16.0.1"
@ -58,8 +58,8 @@ subprojects {
afterEvaluate {project -> afterEvaluate {project ->
if (project.hasProperty("android")) { if (project.hasProperty("android")) {
android { android {
buildToolsVersion "34.0.0" buildToolsVersion "35.0.0"
compileSdkVersion 34 compileSdkVersion 35
defaultConfig { defaultConfig {
minSdkVersion 24 minSdkVersion 24
} }

2
android/gradlew vendored
View file

@ -249,4 +249,4 @@ eval "set -- $(
tr '\n' ' ' tr '\n' ' '
)" '"$@"' )" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

188
android/gradlew.bat vendored
View file

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

View file

@ -1,4 +1,3 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import * as bitcoin from 'bitcoinjs-lib'; import * as bitcoin from 'bitcoinjs-lib';
import DefaultPreference from 'react-native-default-preference'; import DefaultPreference from 'react-native-default-preference';
@ -9,6 +8,9 @@ import { LegacyWallet, SegwitBech32Wallet, SegwitP2SHWallet, TaprootWallet } fro
import presentAlert from '../components/Alert'; import presentAlert from '../components/Alert';
import loc from '../loc'; import loc from '../loc';
import { GROUP_IO_BLUEWALLET } from './currency'; 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 ElectrumClient = require('electrum-client');
const net = require('net'); const net = require('net');
@ -67,17 +69,11 @@ type MempoolTransaction = {
fee: number; fee: number;
}; };
type Peer = type Peer = {
| { host: string;
host: string; ssl?: number;
ssl: string; tcp?: number;
tcp?: undefined; };
}
| {
host: string;
tcp: string;
ssl?: undefined;
};
export const ELECTRUM_HOST = 'electrum_host'; export const ELECTRUM_HOST = 'electrum_host';
export const ELECTRUM_TCP_PORT = 'electrum_tcp_port'; 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'; export const ELECTRUM_SERVER_HISTORY = 'electrum_server_history';
const ELECTRUM_CONNECTION_DISABLED = 'electrum_disabled'; const ELECTRUM_CONNECTION_DISABLED = 'electrum_disabled';
const storageKey = 'ELECTRUM_PEERS'; const storageKey = 'ELECTRUM_PEERS';
const defaultPeer = { host: 'electrum1.bluewallet.io', ssl: '443' }; const defaultPeer = { host: 'electrum1.bluewallet.io', ssl: 443 };
export const hardcodedPeers: Peer[] = [ export const hardcodedPeers: Peer[] = [
{ host: 'mainnet.foundationdevices.com', ssl: '50002' }, { host: 'mainnet.foundationdevices.com', ssl: 50002 },
// { host: 'bitcoin.lukechilds.co', ssl: '50002' }, // { host: 'bitcoin.lukechilds.co', ssl: 50002 },
// { host: 'electrum.jochen-hoenicke.de', ssl: '50006' }, // { host: 'electrum.jochen-hoenicke.de', ssl: '50006' },
{ host: 'electrum1.bluewallet.io', ssl: '443' }, { host: 'electrum1.bluewallet.io', ssl: 443 },
{ host: 'electrum.acinq.co', ssl: '50002' }, { host: 'electrum.acinq.co', ssl: 50002 },
{ host: 'electrum.bitaroo.net', ssl: '50002' }, { host: 'electrum.bitaroo.net', ssl: 50002 },
]; ];
export const suggestedServers: Peer[] = hardcodedPeers.map(peer => ({
...peer,
}));
let mainClient: typeof ElectrumClient | undefined; let mainClient: typeof ElectrumClient | undefined;
let mainConnected: boolean = false; let mainConnected: boolean = false;
let wasConnectedAtLeastOnce: boolean = false; let wasConnectedAtLeastOnce: boolean = false;
@ -131,28 +131,71 @@ async function _getRealm() {
schema, schema,
path, path,
encryptionKey, encryptionKey,
excludeFromIcloudBackup: true,
}); });
return _realm; 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> { export async function isDisabled(): Promise<boolean> {
let result; let result;
try { 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) { if (savedValue === null) {
result = false; result = false;
} else { } else {
result = savedValue; result = savedValue;
} }
} catch { } catch (error) {
console.error('Error getting Electrum connection disabled state:', error);
result = false; result = false;
} }
return !!result; return !!result;
} }
export async function setDisabled(disabled = true) { 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() { function getCurrentPeer() {
@ -170,23 +213,31 @@ function getNextPeer() {
} }
async function getSavedPeer(): Promise<Peer | null> { async function getSavedPeer(): Promise<Peer | null> {
const host = await AsyncStorage.getItem(ELECTRUM_HOST); try {
const tcpPort = await AsyncStorage.getItem(ELECTRUM_TCP_PORT); await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const sslPort = await AsyncStorage.getItem(ELECTRUM_SSL_PORT); 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; return null;
} }
if (sslPort) {
return { host, ssl: sslPort };
}
if (tcpPort) {
return { host, tcp: tcpPort };
}
return null;
} }
export async function connectMain(): Promise<void> { export async function connectMain(): Promise<void> {
@ -200,22 +251,7 @@ export async function connectMain(): Promise<void> {
usingPeer = savedPeer; usingPeer = savedPeer;
} }
await DefaultPreference.setName(GROUP_IO_BLUEWALLET); console.log('Using peer:', JSON.stringify(usingPeer));
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);
}
try { try {
console.log('begin connection:', JSON.stringify(usingPeer)); 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 // most likely got a timeout from electrum ping. lets reconnect
// but only if we were previously connected (mainConnected), otherwise theres other // but only if we were previously connected (mainConnected), otherwise theres other
// code which does connection retries // code which does connection retries
mainClient.close(); mainClient?.close();
mainClient = undefined;
mainConnected = false; mainConnected = false;
// dropping `mainConnected` flag ensures there wont be reconnection race condition if several // dropping `mainConnected` flag ensures there wont be reconnection race condition if several
// errors triggered // errors triggered
@ -275,12 +312,15 @@ export async function connectMain(): Promise<void> {
} catch (e) { } catch (e) {
mainConnected = false; mainConnected = false;
console.log('bad connection:', JSON.stringify(usingPeer), e); console.log('bad connection:', JSON.stringify(usingPeer), e);
mainClient?.close();
mainClient = undefined;
} }
if (!mainConnected) { if (!mainConnected) {
console.log('retry'); console.log('retry');
connectionAttempt = connectionAttempt + 1; connectionAttempt = connectionAttempt + 1;
mainClient.close && mainClient.close(); mainClient?.close();
mainClient = undefined;
if (connectionAttempt >= 5) { if (connectionAttempt >= 5) {
presentNetworkErrorAlert(usingPeer); presentNetworkErrorAlert(usingPeer);
} else { } 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) => { const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
if (await isDisabled()) { if (await isDisabled()) {
console.log( console.log(
@ -298,6 +399,7 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
); );
return; return;
} }
presentAlert({ presentAlert({
allowRepeat: false, allowRepeat: false,
title: loc.errors.network, title: loc.errors.network,
@ -310,7 +412,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
text: loc.wallets.list_tryagain, text: loc.wallets.list_tryagain,
onPress: () => { onPress: () => {
connectionAttempt = 0; connectionAttempt = 0;
mainClient.close() && mainClient.close(); mainClient?.close();
mainClient = undefined;
setTimeout(connectMain, 500); setTimeout(connectMain, 500);
}, },
style: 'default', style: 'default',
@ -318,39 +421,14 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
{ {
text: loc.settings.electrum_reset, text: loc.settings.electrum_reset,
onPress: () => { onPress: () => {
presentAlert({ presentResetToDefaultsAlert().then(result => {
title: loc.settings.electrum_reset, if (result) {
message: loc.settings.electrum_reset_to_default, connectionAttempt = 0;
buttons: [ mainClient?.close();
{ mainClient = undefined;
text: loc._.cancel, setTimeout(connectMain, 500);
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 },
}); });
connectionAttempt = 0;
mainClient.close() && mainClient.close();
}, },
style: 'destructive', style: 'destructive',
}, },
@ -358,7 +436,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
text: loc._.cancel, text: loc._.cancel,
onPress: () => { onPress: () => {
connectionAttempt = 0; connectionAttempt = 0;
mainClient.close() && mainClient.close(); mainClient?.close();
mainClient = undefined;
}, },
style: 'cancel', style: 'cancel',
}, },
@ -376,13 +455,18 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
async function getRandomDynamicPeer(): Promise<Peer> { async function getRandomDynamicPeer(): Promise<Peer> {
try { 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 peers = peers.sort(() => Math.random() - 0.5); // shuffle
for (const peer of peers) { for (const peer of peers) {
const ret = { const ret: Peer = { host: peer[0], ssl: peer[1] };
host: peer[1] as string, ret.host = peer[1];
tcp: '',
}; if (peer[1] === 's') {
ret.ssl = peer[2];
} else {
ret.tcp = peer[2];
}
for (const item of peer[2]) { for (const item of peer[2]) {
if (item.startsWith('t')) { if (item.startsWith('t')) {
ret.tcp = item.replace('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 }> { export const getBalanceByAddress = async function (address: string): Promise<{ confirmed: number; unconfirmed: number }> {
if (!mainClient) throw new Error('Electrum client is not connected'); try {
const script = bitcoin.address.toOutputScript(address); if (!mainClient) throw new Error('Electrum client is not connected');
const hash = bitcoin.crypto.sha256(script); const script = bitcoin.address.toOutputScript(address);
const reversedHash = Buffer.from(hash).reverse(); const hash = bitcoin.crypto.sha256(script);
const balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex')); const reversedHash = Buffer.from(hash).reverse();
balance.addr = address; const balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
return balance; balance.addr = address;
return balance;
} catch (error) {
console.error('Error in getBalanceByAddress:', error);
throw error;
}
}; };
export const getConfig = async function () { export const getConfig = async function () {
@ -882,25 +971,29 @@ export async function multiGetTransactionByTxid<T extends boolean>(
} }
// saving cache: // saving cache:
realm.write(() => { try {
for (const txid of Object.keys(ret)) { realm.write(() => {
const tx = ret[txid]; for (const txid of Object.keys(ret)) {
// dont cache immature txs, but only for 'verbose', since its fully decoded tx jsons. non-verbose are just plain const tx = ret[txid];
// strings txhex // dont cache immature txs, but only for 'verbose', since its fully decoded tx jsons. non-verbose are just plain
if (verbose && typeof tx !== 'string' && (!tx?.confirmations || tx.confirmations < 7)) { // strings txhex
continue; if (verbose && typeof tx !== 'string' && (!tx?.confirmations || tx.confirmations < 7)) {
} continue;
}
realm.create( realm.create(
'Cache', 'Cache',
{ {
cache_key: txid + cacheKeySuffix, cache_key: txid + cacheKeySuffix,
cache_value: JSON.stringify(ret[txid]), cache_value: JSON.stringify(ret[txid]),
}, },
Realm.UpdateMode.Modified, Realm.UpdateMode.Modified,
); );
} }
}); });
} catch (writeError) {
console.error('Failed to write transaction cache:', writeError);
}
return ret; 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 DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs'; 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 Share from 'react-native-share';
import { request, PERMISSIONS } from 'react-native-permissions';
import presentAlert from '../components/Alert'; import presentAlert from '../components/Alert';
import loc from '../loc'; import loc from '../loc';
import { isDesktop } from './environment'; import { isDesktop } from './environment';
@ -16,65 +15,54 @@ const _sanitizeFileName = (fileName: string) => {
}; };
const _shareOpen = async (filePath: string, showShareDialog: boolean = false) => { const _shareOpen = async (filePath: string, showShareDialog: boolean = false) => {
return await Share.open({ try {
url: 'file://' + filePath, await Share.open({
saveToFiles: isDesktop || !showShareDialog, url: 'file://' + filePath,
// @ts-ignore: Website claims this propertie exists, but TS cant find it. Send anyways. saveToFiles: isDesktop || !showShareDialog,
useInternalStorage: Platform.OS === 'android', // @ts-ignore: Website claims this propertie exists, but TS cant find it. Send anyways.
failOnCancel: false, 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);
}); });
} 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 * 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) { export const writeFileAndExport = async function (fileName: string, contents: string, showShareDialog: boolean = true) {
const sanitizedFileName = _sanitizeFileName(fileName); const sanitizedFileName = _sanitizeFileName(fileName);
if (Platform.OS === 'ios') { try {
const filePath = RNFS.TemporaryDirectoryPath + `/${sanitizedFileName}`; if (Platform.OS === 'ios') {
await RNFS.writeFile(filePath, contents); const filePath = `${RNFS.TemporaryDirectoryPath}/${sanitizedFileName}`;
await _shareOpen(filePath, showShareDialog); await RNFS.writeFile(filePath, contents);
} else if (Platform.OS === 'android') { await _shareOpen(filePath, showShareDialog);
const isAndroidVersion33OrAbove = Platform.Version >= 33; } else if (Platform.OS === 'android') {
const permissionType = isAndroidVersion33OrAbove ? PERMISSIONS.ANDROID.READ_MEDIA_IMAGES : PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE; const filePath = `${RNFS.DownloadDirectoryPath}/${sanitizedFileName}`;
request(permissionType).then(async result => { try {
if (result === 'granted') { await RNFS.writeFile(filePath, contents);
const filePath = RNFS.ExternalDirectoryPath + `/${sanitizedFileName}`; if (showShareDialog) {
try { await _shareOpen(filePath);
await RNFS.writeFile(filePath, contents); } else {
if (showShareDialog) { presentAlert({ message: loc.formatString(loc.send.file_saved_at_path, { filePath }) });
await _shareOpen(filePath);
} else {
presentAlert({ message: loc.formatString(loc.send.file_saved_at_path, { filePath }) });
}
} catch (e: any) {
presentAlert({ message: e.message });
} }
} else { } catch (e: any) {
Alert.alert(loc.send.permission_storage_title, loc.send.permission_storage_denied_message, [ console.error(e);
{ presentAlert({ message: e.message });
text: loc.send.open_settings,
onPress: () => {
Linking.openSettings();
},
style: 'default',
},
{ text: loc._.cancel, onPress: () => {}, style: 'cancel' },
]);
} }
}); }
} catch (error: any) {
console.error(error);
presentAlert({ message: error.message });
} }
}; };
@ -110,41 +98,45 @@ const _readPsbtFileIntoBase64 = async function (uri: string): Promise<string> {
} else { } else {
// file was a text file, having base64 psbt in there. so we basically have double base64encoded string // 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; // thats why we are returning string that was decoded once;
// most likely produced by Coldcard // most likely produced by ColdCard
return stringData; return stringData;
} }
}; };
export const showImagePickerAndReadImage = (): Promise<string | undefined> => { export const showImagePickerAndReadImage = async (): Promise<string | undefined> => {
return new Promise((resolve, reject) => try {
launchImageLibrary( const response: ImagePickerResponse = await launchImageLibrary({
{ mediaType: 'photo',
mediaType: 'photo', maxHeight: 800,
maxHeight: 800, maxWidth: 600,
maxWidth: 600, selectionLimit: 1,
selectionLimit: 1, });
},
response => { if (response.didCancel) {
if (!response.didCancel) { return undefined;
const asset = response.assets?.[0] ?? {}; } else if (response.errorCode) {
if (asset.uri) { throw new Error(response.errorMessage);
RNQRGenerator.detect({ } else if (response.assets) {
uri: decodeURI(asset.uri.toString()), try {
}) const uri = response.assets[0].uri;
.then(result => { if (uri) {
if (result) { const result = await RNQRGenerator.detect({ uri: decodeURI(uri.toString()) });
resolve(result.values[0]); if (result?.values.length > 0) {
} return result?.values[0];
})
.catch(error => {
console.error(error);
reject(new Error(loc.send.qr_error_no_qrcode));
});
} }
} }
}, 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 }> { 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) { if (!res.fileCopyUri) {
// to make ts happy, should not need this check here
presentAlert({ message: 'Picking and caching a file failed' }); presentAlert({ message: 'Picking and caching a file failed' });
return { data: false, uri: false }; return { data: false, uri: false };
} }
const fileCopyUri = decodeURI(res.fileCopyUri); const fileCopyUri = decodeURI(res.fileCopyUri);
let file;
if (res.fileCopyUri.toLowerCase().endsWith('.psbt')) { if (res.fileCopyUri.toLowerCase().endsWith('.psbt')) {
// this is either binary file from ElectrumDesktop OR string file with base64 string in there // this is either binary file from ElectrumDesktop OR string file with base64 string in there
file = await _readPsbtFileIntoBase64(fileCopyUri); const file = await _readPsbtFileIntoBase64(fileCopyUri);
return { data: file, uri: decodeURI(res.fileCopyUri) }; return { data: file, uri: fileCopyUri };
} }
if (res.type === DocumentPicker.types.images || res.type?.startsWith('image/')) { if (res.type === DocumentPicker.types.images || res.type?.startsWith('image/')) {
return new Promise(resolve => { return await handleImageFile(fileCopyUri);
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 });
});
});
} }
file = await RNFS.readFile(fileCopyUri); const file = await RNFS.readFile(fileCopyUri);
return { data: file, uri: fileCopyUri }; return { data: file, uri: fileCopyUri };
} catch (err: any) { } catch (err: any) {
if (!DocumentPicker.isCancel(err)) { 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) => { export const readFileOutsideSandbox = (filePath: string) => {
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
return readFile(filePath); 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 PushNotificationIOS from '@react-native-community/push-notification-ios';
import { AppState, Platform } from 'react-native'; import { AppState, Platform } from 'react-native';
import { getApplicationName, getSystemName, getSystemVersion, getVersion, hasGmsSync, hasHmsSync } from 'react-native-device-info'; 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 PushNotification from 'react-native-push-notification';
import loc from '../loc'; import loc from '../loc';
import { groundControlUri } from './constants'; import { groundControlUri } from './constants';
import { fetch } from '../util/fetch';
const PUSH_TOKEN = 'PUSH_TOKEN'; const PUSH_TOKEN = 'PUSH_TOKEN';
const GROUNDCONTROL_BASE_URI = 'GROUNDCONTROL_BASE_URI'; 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 alreadyConfigured = false;
let baseURI = groundControlUri; 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 () => { export const checkNotificationPermissionStatus = async () => {
try { try {
const { status } = await checkNotifications(); const { status } = await checkNotifications();
@ -28,15 +42,14 @@ export const checkNotificationPermissionStatus = async () => {
let currentPermissionStatus = 'unavailable'; let currentPermissionStatus = 'unavailable';
const handleAppStateChange = async nextAppState => { const handleAppStateChange = async nextAppState => {
if (nextAppState === 'active') { if (nextAppState === 'active') {
const newPermissionStatus = await checkNotificationPermissionStatus(); const isDisabledByUser = (await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG)) === 'true';
if (newPermissionStatus !== currentPermissionStatus) { if (!isDisabledByUser) {
currentPermissionStatus = newPermissionStatus; const newPermissionStatus = await checkNotificationPermissionStatus();
if (newPermissionStatus === 'granted') { if (newPermissionStatus !== currentPermissionStatus) {
// Re-initialize notifications if permissions are granted currentPermissionStatus = newPermissionStatus;
await initializeNotifications(); if (newPermissionStatus === 'granted') {
} else { await initializeNotifications();
// Optionally, handle the case where permissions are disabled (e.g., disable in-app notifications) }
console.warn('Notifications have been disabled at the system level.');
} }
} }
} }
@ -64,25 +77,34 @@ export const cleanUserOptOutFlag = async () => {
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
*/ */
export const tryToObtainPermissions = async () => { export const tryToObtainPermissions = async () => {
if (!isNotificationsCapable) return false; console.debug('tryToObtainPermissions: Starting user-triggered permission request');
try { if (!isNotificationsCapable) {
const token = await getPushToken(); console.debug('tryToObtainPermissions: Device not capable');
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);
}
return false; 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 * 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 * @returns {Promise<object>} Response object from API rest call
*/ */
export const majorTomToGroundControl = async (addresses, hashes, txids) => { export const majorTomToGroundControl = async (addresses, hashes, txids) => {
console.debug('majorTomToGroundControl: Starting notification registration', {
addressCount: addresses?.length,
hashCount: hashes?.length,
txidCount: txids?.length,
});
try { try {
const noAndDontAskFlag = await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG); const noAndDontAskFlag = await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG);
if (noAndDontAskFlag === 'true') { if (noAndDontAskFlag === 'true') {
@ -106,6 +134,7 @@ export const majorTomToGroundControl = async (addresses, hashes, txids) => {
} }
const pushToken = await getPushToken(); const pushToken = await getPushToken();
console.debug('majorTomToGroundControl: Retrieved push token:', !!pushToken);
if (!pushToken || !pushToken.token || !pushToken.os) { if (!pushToken || !pushToken.token || !pushToken.os) {
return; return;
} }
@ -120,6 +149,7 @@ export const majorTomToGroundControl = async (addresses, hashes, txids) => {
let response; let response;
try { try {
console.debug('majorTomToGroundControl: Sending request to:', `${baseURI}/majorTomToGroundControl`);
response = await fetch(`${baseURI}/majorTomToGroundControl`, { response = await fetch(`${baseURI}/majorTomToGroundControl`, {
method: 'POST', method: 'POST',
headers: _getHeaders(), headers: _getHeaders(),
@ -160,11 +190,16 @@ export const majorTomToGroundControl = async (addresses, hashes, txids) => {
* @returns {Promise<Object>} * @returns {Promise<Object>}
*/ */
export const checkPermissions = async () => { export const checkPermissions = async () => {
return new Promise(function (resolve) { try {
PushNotification.checkPermissions(result => { return new Promise(function (resolve) {
resolve(result); 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([ await Promise.all([
new Promise(resolve => PushNotification.removeAllDeliveredNotifications(resolve)), new Promise(resolve => PushNotification.removeAllDeliveredNotifications(resolve)),
new Promise(resolve => PushNotification.setApplicationIconBadgeNumber(0, 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'), AsyncStorage.setItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG, 'true'),
]); ]);
console.debug('Notifications disabled successfully'); console.debug('Notifications disabled successfully');
@ -228,12 +263,19 @@ export const addNotification = async notification => {
}; };
const postTokenConfig = async () => { const postTokenConfig = async () => {
console.debug('postTokenConfig: Starting token configuration');
const pushToken = await getPushToken(); 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 { try {
const lang = (await AsyncStorage.getItem('lang')) || 'en'; const lang = (await AsyncStorage.getItem('lang')) || 'en';
const appVersion = getSystemName() + ' ' + getSystemVersion() + ';' + getApplicationName() + ' ' + getVersion(); const appVersion = getSystemName() + ' ' + getSystemVersion() + ';' + getApplicationName() + ' ' + getVersion();
console.debug('postTokenConfig: Posting configuration', { lang, appVersion });
await fetch(`${baseURI}/setTokenConfiguration`, { await fetch(`${baseURI}/setTokenConfiguration`, {
method: 'POST', method: 'POST',
@ -253,8 +295,13 @@ const postTokenConfig = async () => {
}; };
const _setPushToken = async token => { const _setPushToken = async token => {
token = JSON.stringify(token); try {
return AsyncStorage.setItem(PUSH_TOKEN, token); 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>} * @returns {Promise<boolean>}
*/ */
export const configureNotifications = async onProcessNotifications => { export const configureNotifications = async onProcessNotifications => {
if (alreadyConfigured) {
console.debug('configureNotifications: Already configured, skipping');
return true;
}
return new Promise(resolve => { return new Promise(resolve => {
const configure = async () => { const handleRegistration = async token => {
const existingToken = await getPushToken(); if (__DEV__) {
if (existingToken) { console.debug('configureNotifications: Token received:', token);
alreadyConfigured = true; }
console.debug('Notifications already configured with existing token.'); alreadyConfigured = true;
if (__DEV__) { await _setPushToken(token);
console.debug('Existing Token:', existingToken); resolve(true);
} };
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; return;
} }
const rationale = { await addNotification(payload);
title: loc.settings.notifications, notification.finish(PushNotificationIOS.FetchResult.NoData);
message: loc.notifications.would_you_like_to_receive_notifications,
buttonPositive: loc._.ok,
buttonNegative: loc.notifications.no_and_dont_ask,
};
const requestPermissions = Platform.OS === 'ios'; if (payload.foreground && onProcessNotifications) {
await onProcessNotifications();
}
};
requestNotifications(['alert', 'sound', 'badge'], Platform.OS === 'android' ? rationale : undefined) const configure = async () => {
.then(({ status }) => { try {
if (status === 'granted') { const { status } = await checkNotifications();
console.debug('Notification permissions granted.'); if (status !== RESULTS.GRANTED) {
PushNotification.configure({ console.debug('configureNotifications: Permissions not granted');
onRegister: async token => { return resolve(false);
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,
});
if (notification.data?.data) { const existingToken = await getPushToken();
// Validate data before merging if (existingToken) {
const validData = {}; alreadyConfigured = true;
for (const [key, value] of Object.entries(notification.data.data)) { console.debug('Notifications already configured with existing token');
if (value != null) { return resolve(true);
validData[key] = value; }
}
}
Object.assign(payload, validData);
}
payload.data = undefined;
// Ensure required fields exist PushNotification.configure({
if (!payload.title && !payload.message) { onRegister: handleRegistration,
console.warn('Notification missing required fields:', payload); onNotification: handleNotification,
return; onRegistrationError: error => {
} console.error('Registration error:', error);
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.');
resolve(false); resolve(false);
} },
}) permissions: { alert: true, badge: true, sound: true },
.catch(error => { popInitialNotification: true,
console.error('Failed to request notifications permission:', error);
resolve(false);
}); });
} catch (error) {
console.error('Error in configure:', error);
resolve(false);
}
}; };
configure(); configure();
}); });
}; };
const _sleep = async ms => {
return new Promise(resolve => setTimeout(resolve, ms));
};
/** /**
* Validates whether the provided GroundControl URI is valid by pinging it. * 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 * @returns {Promise<boolean>} TRUE if valid, FALSE otherwise
*/ */
export const isGroundControlUriValid = async uri => { export const isGroundControlUriValid = async uri => {
let response;
try { try {
response = await Promise.race([fetch(`${uri}/ping`, { headers: _getHeaders() }), _sleep(2000)]); const response = await fetch(`${uri}/ping`, { headers: _getHeaders() });
} catch (_) {} const json = await response.json();
return !!json.description;
if (!response) return false; } catch (_) {
return false;
const json = await response.json(); }
return !!json.description;
}; };
export const isNotificationsCapable = hasGmsSync() || hasHmsSync() || Platform.OS !== 'android'; export const isNotificationsCapable = hasGmsSync() || hasHmsSync() || Platform.OS !== 'android';
@ -401,24 +425,21 @@ const getLevels = async () => {
const pushToken = await getPushToken(); const pushToken = await getPushToken();
if (!pushToken || !pushToken.token || !pushToken.os) return; if (!pushToken || !pushToken.token || !pushToken.os) return;
let response;
try { try {
response = await Promise.race([ const response = await fetch(`${baseURI}/getTokenConfiguration`, {
fetch(`${baseURI}/getTokenConfiguration`, { method: 'POST',
method: 'POST', headers: _getHeaders(),
headers: _getHeaders(), body: JSON.stringify({
body: JSON.stringify({ token: pushToken.token,
token: pushToken.token, os: pushToken.os,
os: pushToken.os,
}),
}), }),
_sleep(3000), });
]);
} catch (_) {}
if (!response) return {}; if (!response) return {};
return await response.json();
return await response.json(); } catch (_) {
return {};
}
}; };
/** /**
@ -481,16 +502,21 @@ export const clearStoredNotifications = async () => {
}; };
export const getDeliveredNotifications = () => { export const getDeliveredNotifications = () => {
return new Promise(resolve => { try {
PushNotification.getDeliveredNotifications(notifications => resolve(notifications)); return new Promise(resolve => {
}); PushNotification.getDeliveredNotifications(notifications => resolve(notifications));
});
} catch (error) {
console.error('Error getting delivered notifications:', error);
throw error;
}
}; };
export const removeDeliveredNotifications = (identifiers = []) => { export const removeDeliveredNotifications = (identifiers = []) => {
PushNotification.removeDeliveredNotifications(identifiers); PushNotification.removeDeliveredNotifications(identifiers);
}; };
export const setApplicationIconBadgeNumber = function (badges) { export const setApplicationIconBadgeNumber = badges => {
PushNotification.setApplicationIconBadgeNumber(badges); PushNotification.setApplicationIconBadgeNumber(badges);
}; };
@ -503,12 +529,12 @@ export const getDefaultUri = () => {
}; };
export const saveUri = async uri => { export const saveUri = async uri => {
baseURI = uri || groundControlUri;
try { try {
baseURI = uri || groundControlUri;
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, baseURI); await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, baseURI);
} catch (storageError) { } catch (error) {
console.error('Failed to reset URI:', storageError); console.error('Error saving URI:', error);
throw storageError; throw error;
} }
}; };
@ -531,8 +557,20 @@ export const getSavedUri = async () => {
}; };
export const isNotificationsEnabled = async () => { export const isNotificationsEnabled = async () => {
const levels = await getLevels(); try {
return !!(await getPushToken()) && !!levels.level_all; 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 () => { export const getStoredNotifications = async () => {
@ -557,8 +595,11 @@ export const getStoredNotifications = async () => {
// on app launch (load module): // on app launch (load module):
export const initializeNotifications = async onProcessNotifications => { export const initializeNotifications = async onProcessNotifications => {
console.debug('initializeNotifications: Starting initialization');
try { try {
const noAndDontAskFlag = await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG); const noAndDontAskFlag = await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG);
console.debug('initializeNotifications: No ask flag status:', noAndDontAskFlag);
if (noAndDontAskFlag === 'true') { if (noAndDontAskFlag === 'true') {
console.warn('User has opted out of notifications.'); console.warn('User has opted out of notifications.');
return; return;
@ -567,25 +608,37 @@ export const initializeNotifications = async onProcessNotifications => {
const baseUriStored = await AsyncStorage.getItem(GROUNDCONTROL_BASE_URI); const baseUriStored = await AsyncStorage.getItem(GROUNDCONTROL_BASE_URI);
baseURI = baseUriStored || groundControlUri; baseURI = baseUriStored || groundControlUri;
console.debug('Base URI set to:', baseURI); 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(); currentPermissionStatus = await checkNotificationPermissionStatus();
console.warn('currentPermissionStatus', currentPermissionStatus); console.debug('initializeNotifications: Permission status:', currentPermissionStatus);
if (currentPermissionStatus === 'granted' && (await getPushToken())) {
console.debug('Permissions granted and push token exists. Configuring notifications...'); // Handle Android 13+ permissions differently
await configureNotifications(onProcessNotifications); const canProceed =
await postTokenConfig(); 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 { } else {
console.warn('Notifications are disabled at the system level.'); console.debug('Notifications require user action to enable');
} }
} catch (error) { } catch (error) {
console.error('Failed to initialize notifications:', 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
@ -209,7 +209,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",

View file

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

View file

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

View file

@ -1,7 +1,6 @@
import bip21, { TOptions } from 'bip21'; import bip21, { TOptions } from 'bip21';
import * as bitcoin from 'bitcoinjs-lib'; import * as bitcoin from 'bitcoinjs-lib';
import URL from 'url'; import URL from 'url';
import { readFileOutsideSandbox } from '../blue_modules/fs'; import { readFileOutsideSandbox } from '../blue_modules/fs';
import { Chain } from '../models/bitcoinUnits'; import { Chain } from '../models/bitcoinUnits';
import { WatchOnlyWallet } from './'; import { WatchOnlyWallet } from './';
@ -87,9 +86,9 @@ class DeeplinkSchemaMatch {
} else if (wallet.chain === Chain.OFFCHAIN) { } else if (wallet.chain === Chain.OFFCHAIN) {
if (action === 'openSend') { if (action === 'openSend') {
completionHandler([ completionHandler([
'ScanLndInvoiceRoot', 'ScanLNDInvoiceRoot',
{ {
screen: 'ScanLndInvoice', screen: 'ScanLNDInvoice',
params: { params: {
walletID: wallet.getID(), walletID: wallet.getID(),
}, },
@ -157,9 +156,9 @@ class DeeplinkSchemaMatch {
]); ]);
} else if (DeeplinkSchemaMatch.isLightningInvoice(event.url)) { } else if (DeeplinkSchemaMatch.isLightningInvoice(event.url)) {
completionHandler([ completionHandler([
'ScanLndInvoiceRoot', 'ScanLNDInvoiceRoot',
{ {
screen: 'ScanLndInvoice', screen: 'ScanLNDInvoice',
params: { params: {
uri: event.url.replace('://', ':'), uri: event.url.replace('://', ':'),
}, },
@ -182,9 +181,9 @@ class DeeplinkSchemaMatch {
// this might be not just an email but a lightning address // this might be not just an email but a lightning address
// @see https://lightningaddress.com // @see https://lightningaddress.com
completionHandler([ completionHandler([
'ScanLndInvoiceRoot', 'ScanLNDInvoiceRoot',
{ {
screen: 'ScanLndInvoice', screen: 'ScanLNDInvoice',
params: { params: {
uri: event.url, uri: event.url,
}, },
@ -306,9 +305,9 @@ class DeeplinkSchemaMatch {
]; ];
} else { } else {
return [ return [
'ScanLndInvoiceRoot', 'ScanLNDInvoiceRoot',
{ {
screen: 'ScanLndInvoice', screen: 'ScanLNDInvoice',
params: { params: {
uri: uri.lndInvoice, uri: uri.lndInvoice,
walletID: wallet.getID(), walletID: wallet.getID(),
@ -413,6 +412,12 @@ class DeeplinkSchemaMatch {
} }
static bip21encode(address: string, options: TOptions): string { 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) { for (const key in options) {
if (key === 'label' && String(options[key]).replace(' ', '').length === 0) { if (key === 'label' && String(options[key]).replace(' ', '').length === 0) {
delete options[key]; delete options[key];

View file

@ -6,6 +6,7 @@ import CryptoJS from 'crypto-js';
// @ts-ignore theres no types for secp256k1 // @ts-ignore theres no types for secp256k1
import secp256k1 from 'secp256k1'; import secp256k1 from 'secp256k1';
import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api 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 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? // maybe its a watch-only address?
yield { progress: 'watch only' }; yield { progress: 'watch only' };
const watchOnly = new WatchOnlyWallet(); const wo1 = new WatchOnlyWallet();
watchOnly.setSecret(text); wo1.setSecret(text);
if (watchOnly.valid()) { if (wo1.valid()) {
await fetch(watchOnly, true); wo1.init();
yield { wallet: watchOnly }; 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 // electrum p2wpkh-p2sh

View file

@ -310,8 +310,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// then we combine it all together // then we combine it all together
const addresses2fetch = []; 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 // external addresses first
let hasUnconfirmed = false; let hasUnconfirmed = false;
this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; 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 // next, internal addresses
let hasUnconfirmed = false; let hasUnconfirmed = false;
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; 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 // 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 // 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); 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); this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations);
} }
for (const pc of this._receive_payment_codes) { 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: // 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 // 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 tx of Object.values(txdatas)) {
for (const vin of tx.vin) { for (const vin of tx.vin) {
if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { 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 tx of Object.values(txdatas)) {
for (const vin of tx.vin) { for (const vin of tx.vin) {
if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
@ -1417,6 +1420,12 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
if (!this.allowBIP47()) { if (!this.allowBIP47()) {
return false; 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 // 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)) { if (!["m/84'/0'/0'", "m/44'/0'/0'", "m/49'/0'/0'"].includes(this.getDerivationPath() as string)) {
return false; return false;

View file

@ -1,5 +1,6 @@
import b58 from 'bs58check'; import b58 from 'bs58check';
import createHash from 'create-hash'; import createHash from 'create-hash';
import wif from 'wif';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { CreateTransactionResult, CreateTransactionUtxo, Transaction, Utxo } from './types'; import { CreateTransactionResult, CreateTransactionUtxo, Transaction, Utxo } from './types';
@ -211,6 +212,17 @@ export class AbstractWallet {
setSecret(newSecret: string): this { setSecret(newSecret: string): this {
const origSecret = newSecret; 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:', ''); this.secret = newSecret.trim().replace('bitcoin:', '').replace('BITCOIN:', '');
if (this.secret.startsWith('BC1')) this.secret = this.secret.toLowerCase(); if (this.secret.startsWith('BC1')) this.secret = this.secret.toLowerCase();

View file

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

View file

@ -629,7 +629,11 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
hexFingerprint = Buffer.from(hexFingerprint, 'hex').toString('hex'); 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]; let xpub = m[2];
if (xpub.indexOf('/') !== -1) { if (xpub.indexOf('/') !== -1) {
xpub = xpub.substr(0, xpub.indexOf('/')); 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 = { export type LightningTransaction = {
memo?: string; memo?: string;
type?: 'user_invoice' | 'payment_request' | 'bitcoind_tx' | 'paid_invoice'; type?: 'user_invoice' | 'payment_request' | 'bitcoind_tx' | 'paid_invoice';
@ -88,6 +102,12 @@ export type LightningTransaction = {
expire_time?: number; expire_time?: number;
ispaid?: boolean; ispaid?: boolean;
walletID?: string; walletID?: string;
value?: number;
amt?: number;
fee?: number;
payment_preimage?: string;
payment_request?: string;
description?: string;
}; };
export type Transaction = { export type Transaction = {

View file

@ -312,4 +312,9 @@ export class WatchOnlyWallet extends LegacyWallet {
if (this._hdWalletInstance) return this._hdWalletInstance.isSegwit(); if (this._hdWalletInstance) return this._hdWalletInstance.isSegwit();
return super.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 ToolTipMenu from './TooltipMenu';
import { CommonToolTipActions } from '../typings/CommonToolTipActions'; import { CommonToolTipActions } from '../typings/CommonToolTipActions';
import loc from '../loc'; import loc from '../loc';
import { navigationRef } from '../NavigationService'; import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
type AddWalletButtonProps = { type AddWalletButtonProps = {
onPress?: (event: GestureResponderEvent) => void; onPress?: (event: GestureResponderEvent) => void;
@ -23,21 +23,25 @@ const styles = StyleSheet.create({
const AddWalletButton: React.FC<AddWalletButtonProps> = ({ onPress }) => { const AddWalletButton: React.FC<AddWalletButtonProps> = ({ onPress }) => {
const { colors } = useTheme(); const { colors } = useTheme();
const navigation = useExtendedNavigation();
const stylesHook = StyleSheet.create({ const stylesHook = StyleSheet.create({
ball: { ball: {
backgroundColor: colors.buttonBackgroundColor, backgroundColor: colors.buttonBackgroundColor,
}, },
}); });
const onPressMenuItem = useCallback((action: string) => { const onPressMenuItem = useCallback(
switch (action) { (action: string) => {
case CommonToolTipActions.ImportWallet.id: switch (action) {
navigationRef.current?.navigate('AddWalletRoot', { screen: 'ImportWallet' }); case CommonToolTipActions.ImportWallet.id:
break; navigation.navigate('AddWalletRoot', { screen: 'ImportWallet' });
default: break;
break; default:
} break;
}, []); }
},
[navigation],
);
const actions = useMemo(() => [CommonToolTipActions.ImportWallet], []); const actions = useMemo(() => [CommonToolTipActions.ImportWallet], []);

View file

@ -1,24 +1,20 @@
import React, { useCallback } from 'react'; import React from 'react';
import { Keyboard, StyleSheet, TextInput, View } from 'react-native'; import { StyleProp, StyleSheet, TextInput, View, ViewStyle } from 'react-native';
import loc from '../loc'; import loc from '../loc';
import { AddressInputScanButton } from './AddressInputScanButton'; import { AddressInputScanButton } from './AddressInputScanButton';
import { useTheme } from './themes'; import { useTheme } from './themes';
import DeeplinkSchemaMatch from '../class/deeplink-schema-match';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
interface AddressInputProps { interface AddressInputProps {
isLoading?: boolean; isLoading?: boolean;
address?: string; address?: string;
placeholder?: string; placeholder?: string;
onChangeText: (text: string) => void; onChangeText: (text: string) => void;
onBarScanned: (ret: { data?: any }) => void;
scanButtonTapped?: () => void;
launchedBy?: string;
editable?: boolean; editable?: boolean;
inputAccessoryViewID?: string; inputAccessoryViewID?: string;
onBlur?: () => void;
onFocus?: () => void; onFocus?: () => void;
onBlur?: () => void;
testID?: string; testID?: string;
style?: StyleProp<ViewStyle>;
keyboardType?: keyboardType?:
| 'default' | 'default'
| 'numeric' | 'numeric'
@ -41,14 +37,12 @@ const AddressInput = ({
testID = 'AddressInput', testID = 'AddressInput',
placeholder = loc.send.details_address, placeholder = loc.send.details_address,
onChangeText, onChangeText,
onBarScanned,
scanButtonTapped = () => {},
launchedBy,
editable = true, editable = true,
inputAccessoryViewID, inputAccessoryViewID,
onBlur = () => {},
onFocus = () => {}, onFocus = () => {},
onBlur = () => {},
keyboardType = 'default', keyboardType = 'default',
style,
}: AddressInputProps) => { }: AddressInputProps) => {
const { colors } = useTheme(); const { colors } = useTheme();
const stylesHook = StyleSheet.create({ 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 ( return (
<View style={[styles.root, stylesHook.root]}> <View style={[styles.root, stylesHook.root, style]}>
<TextInput <TextInput
testID={testID} testID={testID}
onChangeText={onChangeText} onChangeText={onChangeText}
@ -93,21 +69,13 @@ const AddressInput = ({
multiline={!editable} multiline={!editable}
inputAccessoryViewID={inputAccessoryViewID} inputAccessoryViewID={inputAccessoryViewID}
clearButtonMode="while-editing" clearButtonMode="while-editing"
onBlur={onBlurEditing}
onFocus={onFocus} onFocus={onFocus}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
keyboardType={keyboardType} keyboardType={keyboardType}
onBlur={onBlur}
/> />
{editable ? ( {editable ? <AddressInputScanButton isLoading={isLoading} onChangeText={onChangeText} /> : null}
<AddressInputScanButton
isLoading={isLoading}
launchedBy={launchedBy}
scanButtonTapped={scanButtonTapped}
onBarScanned={onBarScanned}
onChangeText={onChangeText}
/>
) : null}
</View> </View>
); );
}; };
@ -120,8 +88,6 @@ const styles = StyleSheet.create({
minHeight: 44, minHeight: 44,
height: 44, height: 44,
alignItems: 'center', alignItems: 'center',
marginVertical: 8,
marginHorizontal: 18,
borderRadius: 4, borderRadius: 4,
}, },
input: { input: {

View file

@ -3,31 +3,33 @@ import { Image, Keyboard, Platform, StyleSheet, Text } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard'; import Clipboard from '@react-native-clipboard/clipboard';
import ToolTipMenu from './TooltipMenu'; import ToolTipMenu from './TooltipMenu';
import loc from '../loc'; import loc from '../loc';
import { scanQrHelper } from '../helpers/scan-qr';
import { showFilePickerAndReadFile, showImagePickerAndReadImage } from '../blue_modules/fs'; import { showFilePickerAndReadFile, showImagePickerAndReadImage } from '../blue_modules/fs';
import presentAlert from './Alert'; import presentAlert from './Alert';
import { useTheme } from './themes'; import { useTheme } from './themes';
import RNQRGenerator from 'rn-qr-generator'; import RNQRGenerator from 'rn-qr-generator';
import { CommonToolTipActions } from '../typings/CommonToolTipActions'; import { CommonToolTipActions } from '../typings/CommonToolTipActions';
import { useSettings } from '../hooks/context/useSettings'; import { useSettings } from '../hooks/context/useSettings';
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
interface AddressInputScanButtonProps { interface AddressInputScanButtonProps {
isLoading: boolean; isLoading?: boolean;
launchedBy?: string;
scanButtonTapped: () => void;
onBarScanned: (ret: { data?: any }) => void;
onChangeText: (text: string) => void; onChangeText: (text: string) => void;
type?: 'default' | 'link';
testID?: string;
beforePress?: () => Promise<void> | void;
} }
export const AddressInputScanButton = ({ export const AddressInputScanButton = ({
isLoading, isLoading,
launchedBy,
scanButtonTapped,
onBarScanned,
onChangeText, onChangeText,
type = 'default',
testID = 'BlueAddressInputScanQrButton',
beforePress,
}: AddressInputScanButtonProps) => { }: AddressInputScanButtonProps) => {
const { colors } = useTheme(); const { colors } = useTheme();
const { isClipboardGetContentEnabled } = useSettings(); const { isClipboardGetContentEnabled } = useSettings();
const navigation = useExtendedNavigation();
const stylesHook = StyleSheet.create({ const stylesHook = StyleSheet.create({
scan: { scan: {
backgroundColor: colors.scanLabel, backgroundColor: colors.scanLabel,
@ -38,14 +40,17 @@ export const AddressInputScanButton = ({
}); });
const toolTipOnPress = useCallback(async () => { const toolTipOnPress = useCallback(async () => {
await scanButtonTapped(); if (beforePress) {
await beforePress();
}
Keyboard.dismiss(); Keyboard.dismiss();
if (launchedBy) scanQrHelper(launchedBy, true).then(value => onBarScanned({ data: value })); navigation.navigate('ScanQRCode', {
}, [launchedBy, onBarScanned, scanButtonTapped]); showFileImportButton: true,
});
}, [navigation, beforePress]);
const actions = useMemo(() => { const actions = useMemo(() => {
const availableActions = [ const availableActions = [
CommonToolTipActions.ScanQR,
CommonToolTipActions.ChoosePhoto, CommonToolTipActions.ChoosePhoto,
CommonToolTipActions.ImportFile, CommonToolTipActions.ImportFile,
{ {
@ -59,18 +64,11 @@ export const AddressInputScanButton = ({
const onMenuItemPressed = useCallback( const onMenuItemPressed = useCallback(
async (action: string) => { async (action: string) => {
if (onBarScanned === undefined) throw new Error('onBarScanned is required');
switch (action) { switch (action) {
case CommonToolTipActions.ScanQR.id: case CommonToolTipActions.ScanQR.id:
scanButtonTapped(); navigation.navigate('ScanQRCode', {
if (launchedBy) { showFileImportButton: true,
scanQrHelper(launchedBy) });
.then(value => onBarScanned({ data: value }))
.catch(error => {
presentAlert({ message: error.message });
});
}
break; break;
case CommonToolTipActions.PasteFromClipboard.id: case CommonToolTipActions.PasteFromClipboard.id:
try { try {
@ -88,8 +86,7 @@ export const AddressInputScanButton = ({
if (getImage) { if (getImage) {
try { try {
const base64Data = getImage.replace(/^data:image\/jpeg;base64,/, ''); const base64Data = getImage.replace(/^data:image\/(png|jpeg|jpg);base64,/, '');
const values = await RNQRGenerator.detect({ const values = await RNQRGenerator.detect({
base64: base64Data, base64: base64Data,
}); });
@ -135,7 +132,7 @@ export const AddressInputScanButton = ({
} }
Keyboard.dismiss(); Keyboard.dismiss();
}, },
[launchedBy, onBarScanned, onChangeText, scanButtonTapped], [navigation, onChangeText],
); );
const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]); const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]);
@ -145,21 +142,29 @@ export const AddressInputScanButton = ({
actions={actions} actions={actions}
isButton isButton
onPressMenuItem={onMenuItemPressed} onPressMenuItem={onMenuItemPressed}
testID="BlueAddressInputScanQrButton" testID={testID}
disabled={isLoading} disabled={isLoading}
onPress={toolTipOnPress} onPress={toolTipOnPress}
buttonStyle={buttonStyle} buttonStyle={type === 'default' ? buttonStyle : undefined}
accessibilityLabel={loc.send.details_scan} accessibilityLabel={loc.send.details_scan}
accessibilityHint={loc.send.details_scan_hint} accessibilityHint={loc.send.details_scan_hint}
> >
<Image source={require('../img/scan-white.png')} accessible={false} /> {type === 'default' ? (
<Text style={[styles.scanText, stylesHook.scanText]} accessible={false}> <>
{loc.send.details_scan} <Image source={require('../img/scan-white.png')} accessible={false} />
</Text> <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> </ToolTipMenu>
); );
}; };
AddressInputScanButton.displayName = 'AddressInputScanButton';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
scan: { scan: {
height: 36, height: 36,
@ -174,4 +179,8 @@ const styles = StyleSheet.create({
scanText: { scanText: {
marginLeft: 4, 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 { Alert as RNAlert, Platform, ToastAndroid, AlertButton, AlertOptions } from 'react-native';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
import loc from '../loc'; import loc from '../loc';
import { navigationRef } from '../NavigationService';
export enum AlertType { export enum AlertType {
Alert, Alert,
@ -22,7 +23,7 @@ const presentAlert = (() => {
}; };
const showAlert = (title: string | undefined, message: string, buttons: AlertButton[], options: AlertOptions) => { 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); RNAlert.alert(title ?? message, title && message ? message : undefined, buttons, options);
} else { } else {
RNAlert.alert(title ?? '', message, buttons, options); RNAlert.alert(title ?? '', message, buttons, options);

View file

@ -3,7 +3,7 @@ import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat'; import localizedFormat from 'dayjs/plugin/localizedFormat';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; 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 { Badge, Icon, Text } from '@rneui/themed';
import { import {
@ -146,7 +146,9 @@ class AmountInput extends Component {
textInput = React.createRef(); textInput = React.createRef();
handleTextInputOnPress = () => { handleTextInputOnPress = () => {
this.textInput.current.focus(); if (this.textInput && this.textInput.current && typeof this.textInput.current.focus === 'function') {
this.textInput.current.focus();
}
}; };
handleChangeText = text => { handleChangeText = text => {
@ -254,11 +256,15 @@ class AmountInput extends Component {
}); });
return ( return (
<TouchableWithoutFeedback <Pressable
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={loc._.enter_amount} accessibilityLabel={loc._.enter_amount}
disabled={this.props.pointerEvents === 'none'} disabled={this.props.pointerEvents === 'none'}
onPress={() => this.textInput.focus()} onPress={() => {
if (this.textInput && this.textInput.current && typeof this.textInput.current.focus === 'function') {
this.textInput.current.focus();
}
}}
> >
<> <>
<View style={styles.root}> <View style={styles.root}>
@ -340,7 +346,7 @@ class AmountInput extends Component {
</View> </View>
)} )}
</> </>
</TouchableWithoutFeedback> </Pressable>
); );
} }
} }

View file

@ -1,10 +1,9 @@
import React, { forwardRef, useImperativeHandle, useRef, ReactElement, ComponentType, ReactNode } from 'react'; import React, { forwardRef, useImperativeHandle, useRef, ReactElement, ComponentType } from 'react';
import { SheetSize, SizeInfo, TrueSheet, TrueSheetProps } from '@lodev09/react-native-true-sheet'; import { SheetSize, SizeChangeEvent, TrueSheet, TrueSheetProps } from '@lodev09/react-native-true-sheet';
import { Keyboard, StyleSheet, View, TouchableOpacity, Platform, GestureResponderEvent, Text } from 'react-native'; import { Keyboard, Image, StyleSheet, View, TouchableOpacity, Platform, GestureResponderEvent, Text } from 'react-native';
import Ionicons from 'react-native-vector-icons/Ionicons';
import SaveFileButton from './SaveFileButton'; import SaveFileButton from './SaveFileButton';
import { useTheme } from './themes'; import { useTheme } from './themes';
import { Image } from '@rneui/base'; import { Icon } from '@rneui/base';
interface BottomModalProps extends TrueSheetProps { interface BottomModalProps extends TrueSheetProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -15,7 +14,7 @@ interface BottomModalProps extends TrueSheetProps {
footer?: ReactElement | ComponentType<any> | null; footer?: ReactElement | ComponentType<any> | null;
footerDefaultMargins?: boolean | number; footerDefaultMargins?: boolean | number;
onPresent?: () => void; onPresent?: () => void;
onSizeChange?: (size: SizeInfo) => void; onSizeChange?: (event: SizeChangeEvent) => void;
showCloseButton?: boolean; showCloseButton?: boolean;
shareContent?: BottomModalShareContent; shareContent?: BottomModalShareContent;
shareButtonOnPress?: (event: GestureResponderEvent) => void; shareButtonOnPress?: (event: GestureResponderEvent) => void;
@ -57,7 +56,7 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
ref, ref,
) => { ) => {
const trueSheetRef = useRef<TrueSheet>(null); const trueSheetRef = useRef<TrueSheet>(null);
const { colors } = useTheme(); const { colors, closeImage } = useTheme();
const stylesHook = StyleSheet.create({ const stylesHook = StyleSheet.create({
barButton: { barButton: {
backgroundColor: colors.lightButton, backgroundColor: colors.lightButton,
@ -107,7 +106,12 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
testID="ModalShareButton" testID="ModalShareButton"
key="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>, </SaveFileButton>,
); );
} else if (shareButtonOnPress) { } else if (shareButtonOnPress) {
@ -118,7 +122,12 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
style={[styles.topRightButton, stylesHook.barButton]} style={[styles.topRightButton, stylesHook.barButton]}
onPress={shareButtonOnPress} 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>, </TouchableOpacity>,
); );
} }
@ -131,11 +140,7 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
key="ModalDoneButton" key="ModalDoneButton"
testID="ModalDoneButton" testID="ModalDoneButton"
> >
{Platform.OS === 'ios' ? ( <Image source={closeImage} />
<Ionicons name="close" size={20} color={colors.buttonTextColor} />
) : (
<Image source={require('../img/close.png')} style={styles.closeButton} />
)}
</TouchableOpacity>, </TouchableOpacity>,
); );
} }
@ -155,18 +160,26 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
); );
} }
return ( if (showCloseButton || shareContent)
<View style={styles.headerContainer}> return (
<View style={styles.headerContent}>{typeof header === 'function' ? <header /> : header}</View> <View style={styles.headerContainer}>
{renderTopRightButton()} <View style={styles.headerContent}>{typeof header === 'function' ? <header /> : header}</View>
</View> {renderTopRightButton()}
); </View>
);
if (React.isValidElement(header)) {
return (
<View style={styles.headerContainerWithCloseButton}>
{header}
{renderTopRightButton()}
</View>
);
}
return null;
}; };
const renderFooter = (): ReactElement | undefined => { const renderFooter = (): ReactElement | undefined => {
// Footer is not working correctly on Android yet.
if (!footer) return undefined;
if (React.isValidElement(footer)) { if (React.isValidElement(footer)) {
return footerDefaultMargins ? <View style={styles.footerContainer}>{footer}</View> : footer; return footerDefaultMargins ? <View style={styles.footerContainer}>{footer}</View> : footer;
} else if (typeof footer === 'function') { } else if (typeof footer === 'function') {
@ -177,7 +190,7 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
return undefined; return undefined;
}; };
const FooterComponent = Platform.OS !== 'android' && renderFooter(); const FooterComponent = renderFooter();
return ( return (
<TrueSheet <TrueSheet
@ -191,7 +204,6 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
{...props} {...props}
> >
<View style={styles.childrenContainer}>{children}</View> <View style={styles.childrenContainer}>{children}</View>
{Platform.OS === 'android' && (renderFooter() as ReactNode)}
{renderHeader()} {renderHeader()}
</TrueSheet> </TrueSheet>
); );
@ -214,6 +226,17 @@ const styles = StyleSheet.create({
right: 16, right: 16,
top: 16, top: 16,
}, },
headerContainerWithCloseButton: {
position: 'absolute',
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
minHeight: 22,
width: '100%',
top: 16,
left: 0,
justifyContent: 'space-between',
},
headerContent: { headerContent: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
@ -223,10 +246,6 @@ const styles = StyleSheet.create({
fontSize: 18, fontSize: 18,
fontWeight: 'bold', fontWeight: 'bold',
}, },
closeButton: {
width: 10,
height: 10,
},
headerSubtitle: { headerSubtitle: {
fontSize: 14, 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 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 A from '../../blue_modules/analytics';
import { BlueApp as BlueAppClass, LegacyWallet, TCounterpartyMetadata, TTXMetadata, WatchOnlyWallet } from '../../class'; import { BlueApp as BlueAppClass, LegacyWallet, TCounterpartyMetadata, TTXMetadata, WatchOnlyWallet } from '../../class';
import type { TWallet } from '../../class/wallets/types'; import type { TWallet } from '../../class/wallets/types';
import presentAlert from '../../components/Alert'; import presentAlert from '../../components/Alert';
import loc from '../../loc'; import loc, { formatBalanceWithoutSuffix } from '../../loc';
import * as BlueElectrum from '../../blue_modules/BlueElectrum'; import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { startAndDecrypt } from '../../blue_modules/start-and-decrypt'; 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(); const BlueApp = BlueAppClass.getInstance();
@ -49,6 +50,8 @@ interface StorageContextType {
cachedPassword: typeof BlueApp.cachedPassword; cachedPassword: typeof BlueApp.cachedPassword;
getItem: typeof BlueApp.getItem; getItem: typeof BlueApp.getItem;
setItem: typeof BlueApp.setItem; setItem: typeof BlueApp.setItem;
handleWalletDeletion: (walletID: string, forceDelete?: boolean) => Promise<boolean>;
confirmWalletDeletion: (wallet: any, onConfirmed: () => void) => void;
} }
export enum WalletTransactionsStatus { export enum WalletTransactionsStatus {
@ -99,6 +102,120 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
setWallets([...BlueApp.getWallets()]); 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(() => { const resetWallets = useCallback(() => {
setWallets(BlueApp.getWallets()); setWallets(BlueApp.getWallets());
}, []); }, []);
@ -120,56 +237,72 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
} }
}, [walletsInitialized]); }, [walletsInitialized]);
// Add a refresh lock to prevent concurrent refreshes
const refreshingRef = useRef<boolean>(false);
const refreshAllWalletTransactions = useCallback( const refreshAllWalletTransactions = useCallback(
async (lastSnappedTo?: number, showUpdateStatusIndicator: boolean = true) => { 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) => const timeoutPromise = new Promise<never>((_resolve, reject) =>
setTimeout(() => { setTimeout(() => {
reject(new Error('refreshAllWalletTransactions: Timeout reached')); console.debug('[refreshAllWalletTransactions] Timeout reached');
reject(new Error('Timeout reached'));
}, TIMEOUT_DURATION), }, TIMEOUT_DURATION),
); );
const mainLogicPromise = new Promise<void>((resolve, reject) => { try {
InteractionManager.runAfterInteractions(async () => { if (showUpdateStatusIndicator) {
let noErr = true; console.debug('[refreshAllWalletTransactions] Setting wallet transaction status to ALL');
try { setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
await BlueElectrum.waitTillConnected(); }
if (showUpdateStatusIndicator) { console.debug('[refreshAllWalletTransactions] Waiting for connectivity...');
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL); await BlueElectrum.waitTillConnected();
} console.debug('[refreshAllWalletTransactions] Connected to Electrum');
const paymentCodesStart = Date.now();
await BlueApp.fetchSenderPaymentCodes(lastSnappedTo);
const paymentCodesEnd = Date.now();
console.debug('fetch payment codes took', (paymentCodesEnd - paymentCodesStart) / 1000, 'sec');
// 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(); const balanceStart = Date.now();
await BlueApp.fetchWalletBalances(lastSnappedTo); await BlueApp.fetchWalletBalances(lastSnappedTo);
const balanceEnd = Date.now(); 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); await BlueApp.fetchWalletTransactions(lastSnappedTo);
const end = Date.now(); const txEnd = Date.now();
console.debug('fetch tx took', (end - start) / 1000, 'sec'); console.debug('[refreshAllWalletTransactions] fetch tx took', (txEnd - txStart) / 1000, 'sec');
} catch (err) {
noErr = false;
console.error(err);
reject(err);
} finally {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
}
if (noErr) await saveToDisk();
resolve();
});
});
try { console.debug('[refreshAllWalletTransactions] Saving data to disk');
await Promise.race([mainLogicPromise, timeoutPromise]); await saveToDisk();
} catch (err) { })(),
console.error('Error in refreshAllWalletTransactions:', err); timeoutPromise,
]);
console.debug('[refreshAllWalletTransactions] Refresh completed successfully');
} catch (error) {
console.error('[refreshAllWalletTransactions] Error:', error);
} finally { } finally {
console.debug('[refreshAllWalletTransactions] Resetting wallet transaction status and refresh lock');
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE); setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
refreshingRef.current = false;
} }
}, },
[saveToDisk], [saveToDisk],
@ -182,24 +315,26 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
let noErr = true; let noErr = true;
try { try {
if (Date.now() - (_lastTimeTriedToRefetchWallet[walletID] || 0) < 5000) { 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; return;
} }
_lastTimeTriedToRefetchWallet[walletID] = Date.now(); _lastTimeTriedToRefetchWallet[walletID] = Date.now();
await BlueElectrum.waitTillConnected(); await BlueElectrum.waitTillConnected();
setWalletTransactionUpdateStatus(walletID); setWalletTransactionUpdateStatus(walletID);
const balanceStart = Date.now(); const balanceStart = Date.now();
await BlueApp.fetchWalletBalances(index); await BlueApp.fetchWalletBalances(index);
const balanceEnd = Date.now(); const balanceEnd = Date.now();
console.debug('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec'); console.debug('[fetchAndSaveWalletTransactions] fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
const start = Date.now();
const txStart = Date.now();
await BlueApp.fetchWalletTransactions(index); await BlueApp.fetchWalletTransactions(index);
const end = Date.now(); const txEnd = Date.now();
console.debug('fetch tx took', (end - start) / 1000, 'sec'); console.debug('[fetchAndSaveWalletTransactions] fetch tx took', (txEnd - txStart) / 1000, 'sec');
} catch (err) { } catch (err) {
noErr = false; noErr = false;
console.error(err); console.error('[fetchAndSaveWalletTransactions] Error:', err);
} finally { } finally {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE); setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
} }
@ -217,10 +352,10 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
return; return;
} }
const emptyWalletLabel = new LegacyWallet().getLabel(); const emptyWalletLabel = new LegacyWallet().getLabel();
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
if (w.getLabel() === emptyWalletLabel) w.setLabel(loc.wallets.import_imported + ' ' + w.typeReadable); if (w.getLabel() === emptyWalletLabel) w.setLabel(loc.wallets.import_imported + ' ' + w.typeReadable);
w.setUserHasSavedExport(true); w.setUserHasSavedExport(true);
addWallet(w); addWallet(w);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
await saveToDisk(); await saveToDisk();
A(A.ENUM.CREATED_WALLET); A(A.ENUM.CREATED_WALLET);
presentAlert({ presentAlert({
@ -239,6 +374,36 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
[wallets, addWallet, saveToDisk], [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( const value: StorageContextType = useMemo(
() => ({ () => ({
wallets, wallets,
@ -274,6 +439,8 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
isPasswordInUse: BlueApp.isPasswordInUse, isPasswordInUse: BlueApp.isPasswordInUse,
walletTransactionUpdateStatus, walletTransactionUpdateStatus,
setWalletTransactionUpdateStatus, setWalletTransactionUpdateStatus,
handleWalletDeletion,
confirmWalletDeletion,
}), }),
[ [
wallets, wallets,
@ -291,7 +458,7 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
refreshAllWalletTransactions, refreshAllWalletTransactions,
resetWallets, resetWallets,
walletTransactionUpdateStatus, 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 { Animated, Dimensions, PixelRatio, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from './themes'; import { useTheme } from './themes';
const BORDER_RADIUS = 8; const BORDER_RADIUS = 8;
const PADDINGS = 24; const PADDINGS = 24;
const ICON_MARGIN = 7; 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: { root: {
alignSelf: 'center', alignSelf: 'center',
height: '6.9%', height: '6.9%',
@ -26,6 +30,27 @@ const cStyles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
overflow: 'hidden', 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 { interface FContainerProps {
@ -51,93 +76,79 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
}).start(); }).start();
}, [height, slideAnimation]); }, [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 } } }) => { const onLayout = (event: { nativeEvent: { layout: { width: number } } }) => {
if (layoutCalculated.current) return; if (layoutCalculated.current) return;
const maxWidth = width - BORDER_RADIUS - 140; const { width: layoutWidth } = event.nativeEvent.layout;
const layoutWidth = event.nativeEvent.layout.width; const totalChildren = React.Children.toArray(props.children).filter(Boolean).length;
const withPaddings = Math.ceil(layoutWidth + PADDINGS * 2); setNewWidth(computeNewWidth(layoutWidth, totalChildren));
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);
layoutCalculated.current = true; 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 ( return (
<Animated.View <Animated.View
ref={ref} ref={ref}
onLayout={onLayout} onLayout={onLayout}
style={[ style={[
cStyles.root, containerStyles.root,
props.inline ? cStyles.rootInline : cStyles.rootAbsolute, props.inline ? containerStyles.rootInline : containerStyles.rootAbsolute,
bottomInsets, bottomInsets,
newWidth ? cStyles.rootPost : cStyles.rootPre, newWidth ? containerStyles.rootPost : containerStyles.rootPre,
totalChildren === 1 ? containerStyles.rootRound : null,
{ transform: [{ translateY: slideAnimation }] }, { transform: [{ translateY: slideAnimation }] },
]} ]}
> >
{newWidth {newWidth ? React.Children.toArray(props.children).filter(Boolean).map(renderChild) : props.children}
? 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}
</Animated.View> </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 { interface FButtonProps {
text: string; text: string;
icon: ReactNode; icon: ReactNode;
width?: number; width?: number;
first?: boolean; first?: boolean;
last?: boolean; last?: boolean;
singleChild?: boolean;
disabled?: boolean; disabled?: boolean;
testID?: string; testID?: string;
onPress: () => void; onPress: () => void;
onLongPress?: () => 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 { colors } = useTheme();
const bStylesHook = StyleSheet.create({ const customButtonStyles = StyleSheet.create({
root: { root: {
backgroundColor: colors.buttonBackgroundColor, backgroundColor: colors.buttonBackgroundColor,
borderRadius: BORDER_RADIUS, borderRadius: BORDER_RADIUS,
@ -151,9 +162,12 @@ export const FButton = ({ text, icon, width, first, last, testID, ...props }: FB
marginRight: { marginRight: {
marginRight: 10, marginRight: 10,
}, },
rootRound: {
borderRadius: 9999,
},
}); });
const style: Record<string, any> = {}; const style: Record<string, any> = {};
const additionalStyles = !last ? bStylesHook.marginRight : {}; const additionalStyles = !last ? customButtonStyles.marginRight : {};
if (width) { if (width) {
style.paddingHorizontal = PADDINGS; style.paddingHorizontal = PADDINGS;
@ -165,11 +179,15 @@ export const FButton = ({ text, icon, width, first, last, testID, ...props }: FB
accessibilityLabel={text} accessibilityLabel={text}
accessibilityRole="button" accessibilityRole="button"
testID={testID} testID={testID}
style={[bStyles.root, bStylesHook.root, style, additionalStyles]} style={[buttonStyles.root, customButtonStyles.root, style, additionalStyles, singleChild ? customButtonStyles.rootRound : null]}
{...props} {...props}
> >
<View style={bStyles.icon}>{icon}</View> <View style={buttonStyles.icon}>{icon}</View>
<Text numberOfLines={1} adjustsFontSizeToFit style={[bStyles.text, props.disabled ? bStylesHook.textDisabled : bStylesHook.text]}> <Text
numberOfLines={1}
adjustsFontSizeToFit
style={[buttonStyles.text, props.disabled ? customButtonStyles.textDisabled : customButtonStyles.text]}
>
{text} {text}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>

View file

@ -9,7 +9,12 @@ import { HandOffComponentProps } from './types';
const HandOffComponent: React.FC<HandOffComponentProps> = props => { const HandOffComponent: React.FC<HandOffComponentProps> = props => {
const { isHandOffUseEnabled } = useSettings(); 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; return isHandOffUseEnabled ? <Handoff {...props} /> : null;
}; };

View file

@ -1,11 +1,13 @@
import React, { useCallback } from 'react'; import React, { useCallback, useState, useEffect, useRef } from 'react';
import { View, StyleSheet, ViewStyle, TouchableOpacity } from 'react-native'; import { StyleSheet, ViewStyle, TouchableOpacity, ActivityIndicator, Platform, Animated } from 'react-native';
import { Icon, ListItem } from '@rneui/base'; import { Icon, ListItem } from '@rneui/base';
import { ExtendedTransaction, LightningTransaction, TWallet } from '../class/wallets/types'; import { ExtendedTransaction, LightningTransaction, TWallet } from '../class/wallets/types';
import { WalletCarouselItem } from './WalletsCarousel'; import { WalletCarouselItem } from './WalletsCarousel';
import { TransactionListItem } from './TransactionListItem'; import { TransactionListItem } from './TransactionListItem';
import { useTheme } from './themes'; import { useTheme } from './themes';
import { BitcoinUnit } from '../models/bitcoinUnits'; import { BitcoinUnit } from '../models/bitcoinUnits';
import loc from '../loc';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
enum ItemType { enum ItemType {
WalletSection = 'wallet', WalletSection = 'wallet',
@ -29,11 +31,16 @@ interface ManageWalletsListItemProps {
isDraggingDisabled: boolean; isDraggingDisabled: boolean;
drag?: () => void; drag?: () => void;
isPlaceHolder?: boolean; isPlaceHolder?: boolean;
onPressIn?: () => void;
onPressOut?: () => void;
state: { wallets: TWallet[]; searchQuery: string }; state: { wallets: TWallet[]; searchQuery: string };
navigateToWallet: (wallet: TWallet) => void; navigateToWallet: (wallet: TWallet) => void;
renderHighlightedText: (text: string, query: string) => JSX.Element; renderHighlightedText: (text: string, query: string) => JSX.Element;
handleDeleteWallet: (wallet: TWallet) => void; handleDeleteWallet: (wallet: TWallet) => void;
handleToggleHideBalance: (wallet: TWallet) => void; handleToggleHideBalance: (wallet: TWallet) => void;
isActive?: boolean;
style?: ViewStyle;
globalDragActive?: boolean;
} }
interface SwipeContentProps { interface SwipeContentProps {
@ -46,14 +53,21 @@ const LeftSwipeContent: React.FC<SwipeContentProps> = ({ onPress, hideBalance, c
<TouchableOpacity <TouchableOpacity
onPress={onPress} onPress={onPress}
style={[styles.leftButtonContainer, { backgroundColor: colors.buttonAlternativeTextColor } as ViewStyle]} 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" /> <Icon name={hideBalance ? 'eye-slash' : 'eye'} color={colors.brandingColor} type="font-awesome-5" />
</TouchableOpacity> </TouchableOpacity>
); );
const RightSwipeContent: React.FC<Partial<SwipeContentProps>> = ({ onPress }) => ( const RightSwipeContent: React.FC<Partial<SwipeContentProps>> = ({ onPress }) => (
<TouchableOpacity onPress={onPress} style={styles.rightButtonContainer as ViewStyle}> <TouchableOpacity
<Icon name="delete-outline" color="#FFFFFF" /> onPress={onPress}
style={styles.rightButtonContainer as ViewStyle}
accessibilityRole="button"
accessibilityLabel="Delete Wallet"
>
<Icon name={Platform.OS === 'android' ? 'delete' : 'delete-outline'} color="#FFFFFF" />
</TouchableOpacity> </TouchableOpacity>
); );
@ -67,68 +81,141 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
renderHighlightedText, renderHighlightedText,
handleDeleteWallet, handleDeleteWallet,
handleToggleHideBalance, handleToggleHideBalance,
onPressIn,
onPressOut,
isActive,
globalDragActive,
style,
}) => { }) => {
const { colors } = useTheme(); 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(() => { const onPress = useCallback(() => {
if (item.type === ItemType.WalletSection) { if (item.type === ItemType.WalletSection) {
setIsLoading(true);
navigateToWallet(item.data); navigateToWallet(item.data);
setIsLoading(false);
} }
}, [item, navigateToWallet]); }, [item, navigateToWallet]);
const leftContent = useCallback( const handleLeftPress = (reset: () => void) => {
(reset: () => void) => ( handleToggleHideBalance(item.data as TWallet);
<LeftSwipeContent reset();
onPress={() => { };
handleToggleHideBalance(item.data as TWallet);
reset();
}}
hideBalance={(item.data as TWallet).hideBalance}
colors={colors}
/>
),
[colors, handleToggleHideBalance, item.data],
);
const rightContent = useCallback( const leftContent = (reset: () => void) => {
(reset: () => void) => ( resetFunctionRef.current = reset;
<RightSwipeContent return <LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />;
onPress={() => { };
handleDeleteWallet(item.data as TWallet);
reset(); const handleRightPress = (reset: () => void) => {
}} reset();
/>
), setTimeout(() => {
[handleDeleteWallet, item.data], 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) { 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 ( return (
<ListItem.Swipeable <Animated.View style={animatedStyle}>
leftWidth={80} <ListItem.Swipeable
rightWidth={90} leftWidth={swipeDisabled ? 0 : 80}
containerStyle={{ backgroundColor: colors.background }} rightWidth={swipeDisabled ? 0 : 90}
leftContent={leftContent} containerStyle={[style, { backgroundColor }, swipeDisabled ? styles.transparentBackground : {}]}
rightContent={rightContent} leftContent={swipeDisabled ? null : leftContent}
> rightContent={swipeDisabled ? null : rightContent}
<ListItem.Content onPressOut={onPressOut}
style={{ minSlideWidth={swipeDisabled ? 0 : 80}
backgroundColor: colors.background, 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 <WalletCarouselItem
item={item.data} item={item.data}
handleLongPress={isDraggingDisabled ? undefined : drag} handleLongPress={isDraggingDisabled || isSwipeActive ? undefined : startDrag}
onPress={onPress} onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
animationsEnabled={false} animationsEnabled={false}
searchQuery={state.searchQuery} searchQuery={state.searchQuery}
isPlaceHolder={isPlaceHolder} isPlaceHolder={isPlaceHolder}
renderHighlightedText={renderHighlightedText} renderHighlightedText={renderHighlightedText}
customStyle={styles.carouselItem}
/> />
</View> </ListItem.Content>
</ListItem.Content> </ListItem.Swipeable>
</ListItem.Swipeable> </Animated.View>
); );
} else if (item.type === ItemType.TransactionSection && item.data) { } else if (item.type === ItemType.TransactionSection && item.data) {
const w = state.wallets.find(wallet => wallet.getTransactions().some((tx: ExtendedTransaction) => tx.hash === item.data.hash)); 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; return null;
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
walletCarouselItemContainer: {
width: '100%',
},
leftButtonContainer: { leftButtonContainer: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
carouselItem: {
width: '100%',
},
rightButtonContainer: { rightButtonContainer: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: 'red', 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 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 { Icon } from '@rneui/themed';
import ActionSheet from '../screen/ActionSheet'; import ActionSheet from '../screen/ActionSheet';
import ActionSheetOptions from '../screen/ActionSheet.common';
import { useTheme } from './themes'; import { useTheme } from './themes';
export const MultipleStepsListItemDashType = Object.freeze({ none: 0, top: 1, bottom: 2, topAndBottom: 3 }); import { ActionSheetOptions } from '../screen/ActionSheet.common';
export const MultipleStepsListItemButtohType = Object.freeze({ partial: 0, full: 1 });
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 { colors } = useTheme();
const { const {
showActivityIndicator = false, showActivityIndicator = false,
dashes = MultipleStepsListItemDashType.none, dashes = MultipleStepsListItemDashType.None,
circledText = '', circledText = '',
leftText = '', leftText = '',
checked = false, checked = false,
useActionSheet = false, isActionSheet = false,
actionSheetOptions = null, // Default to null or appropriate default actionSheetOptions = null, // Default to null or appropriate default
} = props; } = props;
const stylesHook = StyleSheet.create({ const stylesHook = StyleSheet.create({
@ -43,7 +86,7 @@ const MultipleStepsListItem = props => {
const selfRef = useRef(null); // Create a ref for the component itself const selfRef = useRef(null); // Create a ref for the component itself
const handleOnPressForActionSheet = () => { const handleOnPressForActionSheet = () => {
if (useActionSheet && actionSheetOptions) { if (isActionSheet && actionSheetOptions) {
// Clone options to modify them // Clone options to modify them
let modifiedOptions = { ...actionSheetOptions }; let modifiedOptions = { ...actionSheetOptions };
@ -57,16 +100,16 @@ const MultipleStepsListItem = props => {
ActionSheet.showActionSheetWithOptions(modifiedOptions, buttonIndex => { ActionSheet.showActionSheetWithOptions(modifiedOptions, buttonIndex => {
// Call the original onPress function, if provided, and not cancelled // 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); props.button.onPress(buttonIndex);
} }
}); });
} }
}; };
const renderDashes = () => { const renderDashes = (): StyleProp<ViewStyle> => {
switch (dashes) { switch (dashes) {
case MultipleStepsListItemDashType.topAndBottom: case MultipleStepsListItemDashType.TopAndBottom:
return { return {
width: 1, width: 1,
borderStyle: 'dashed', borderStyle: 'dashed',
@ -77,7 +120,7 @@ const MultipleStepsListItem = props => {
marginLeft: 20, marginLeft: 20,
position: 'absolute', position: 'absolute',
}; };
case MultipleStepsListItemDashType.bottom: case MultipleStepsListItemDashType.Bottom:
return { return {
width: 1, width: 1,
borderStyle: 'dashed', borderStyle: 'dashed',
@ -88,7 +131,7 @@ const MultipleStepsListItem = props => {
marginLeft: 20, marginLeft: 20,
position: 'absolute', position: 'absolute',
}; };
case MultipleStepsListItemDashType.top: case MultipleStepsListItemDashType.Top:
return { return {
width: 1, width: 1,
borderStyle: 'dashed', borderStyle: 'dashed',
@ -105,6 +148,7 @@ const MultipleStepsListItem = props => {
}; };
const buttonOpacity = { opacity: props.button?.disabled ? 0.5 : 1.0 }; const buttonOpacity = { opacity: props.button?.disabled ? 0.5 : 1.0 };
const rightButtonOpacity = { opacity: props.rightButton?.disabled ? 0.5 : 1.0 }; const rightButtonOpacity = { opacity: props.rightButton?.disabled ? 0.5 : 1.0 };
const onPress = isActionSheet ? handleOnPressForActionSheet : props.button?.onPress;
return ( return (
<View> <View>
<View style={renderDashes()} /> <View style={renderDashes()} />
@ -131,19 +175,19 @@ const MultipleStepsListItem = props => {
{!showActivityIndicator && props.button && ( {!showActivityIndicator && props.button && (
<> <>
{props.button.buttonType === undefined || {props.button.buttonType === undefined ||
(props.button.buttonType === MultipleStepsListItemButtohType.full && ( (props.button.buttonType === MultipleStepsListItemButtonType.Full && (
<TouchableOpacity <TouchableOpacity
ref={useActionSheet ? selfRef : null} ref={isActionSheet ? selfRef : null}
testID={props.button.testID} testID={props.button.testID}
accessibilityRole="button" accessibilityRole="button"
disabled={props.button.disabled} disabled={props.button.disabled}
style={[styles.provideKeyButton, stylesHook.provideKeyButton, buttonOpacity]} style={[styles.provideKeyButton, stylesHook.provideKeyButton, buttonOpacity]}
onPress={useActionSheet ? handleOnPressForActionSheet : props.button.onPress} onPress={onPress}
> >
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.button.text}</Text> <Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.button.text}</Text>
</TouchableOpacity> </TouchableOpacity>
))} ))}
{props.button.buttonType === MultipleStepsListItemButtohType.partial && ( {props.button.buttonType === MultipleStepsListItemButtonType.Partial && (
<View style={styles.buttonPartialContainer}> <View style={styles.buttonPartialContainer}>
<Text numberOfLines={1} style={[styles.rowPartialLeftText, stylesHook.rowPartialLeftText]} lineBreakMode="middle"> <Text numberOfLines={1} style={[styles.rowPartialLeftText, stylesHook.rowPartialLeftText]} lineBreakMode="middle">
{props.button.leftText} {props.button.leftText}
@ -153,11 +197,15 @@ const MultipleStepsListItem = props => {
accessibilityRole="button" accessibilityRole="button"
disabled={props.button.disabled} disabled={props.button.disabled}
style={[styles.rowPartialRightButton, stylesHook.provideKeyButton, rightButtonOpacity]} style={[styles.rowPartialRightButton, stylesHook.provideKeyButton, rightButtonOpacity]}
onPress={props.button.onPress} onPress={onPress}
> >
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText, styles.rightButton]}> {props.button.showActivityIndicator ? (
{props.button.text} <ActivityIndicator />
</Text> ) : (
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText, styles.rightButton]}>
{props.button.text}
</Text>
)}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} )}
@ -171,7 +219,11 @@ const MultipleStepsListItem = props => {
style={styles.rightButton} style={styles.rightButton}
onPress={props.rightButton.onPress} 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> </TouchableOpacity>
</View> </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({ const styles = StyleSheet.create({
container: { container: {
flexDirection: 'row', flexDirection: 'row',

View file

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

View file

@ -20,6 +20,8 @@ interface QRCodeComponentProps {
onError?: () => void; onError?: () => void;
} }
const BORDER_WIDTH = 6;
const actionIcons: { [key: string]: ActionIcons } = { const actionIcons: { [key: string]: ActionIcons } = {
Share: { Share: {
iconValue: 'square.and.arrow.up', iconValue: 'square.and.arrow.up',
@ -62,7 +64,7 @@ const QRCodeComponent: React.FC<QRCodeComponentProps> = ({
onError = () => {}, onError = () => {},
}) => { }) => {
const qrCode = useRef<any>(); const qrCode = useRef<any>();
const { colors } = useTheme(); const { colors, dark } = useTheme();
const handleShareQRCode = () => { const handleShareQRCode = () => {
qrCode.current.toDataURL((data: string) => { 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 = ( const renderQRCode = (
<QRCode <QRCode
value={value} value={value}
{...(isLogoRendered ? { logo: require('../img/qr-code.png') } : {})} {...(isLogoRendered ? { logo: require('../img/qr-code.png') } : {})}
size={size} size={newSize}
logoSize={logoSize} logoSize={logoSize}
color="#000000" color="#000000"
logoBackgroundColor={colors.brandingColor} logoBackgroundColor={colors.brandingColor}
@ -99,7 +107,7 @@ const QRCodeComponent: React.FC<QRCodeComponentProps> = ({
return ( return (
<View <View
style={styles.qrCodeContainer} style={[styles.container, stylesHook.container]}
testID="BitcoinAddressQRCodeContainer" testID="BitcoinAddressQRCodeContainer"
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
importantForAccessibility="no-hide-descendants" importantForAccessibility="no-hide-descendants"
@ -120,5 +128,5 @@ const QRCodeComponent: React.FC<QRCodeComponentProps> = ({
export default QRCodeComponent; export default QRCodeComponent;
const styles = StyleSheet.create({ 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'; import { requireNativeComponent, View, StyleSheet, NativeSyntheticEvent } from 'react-native';
interface SegmentedControlProps { interface SegmentedControlProps {
@ -21,9 +21,18 @@ interface NativeSegmentedControlProps {
const NativeSegmentedControl = requireNativeComponent<NativeSegmentedControlProps>('CustomSegmentedControl'); const NativeSegmentedControl = requireNativeComponent<NativeSegmentedControlProps>('CustomSegmentedControl');
const SegmentedControl: React.FC<SegmentedControlProps> = ({ values, selectedIndex, onChange }) => { const SegmentedControl: React.FC<SegmentedControlProps> = ({ values, selectedIndex, onChange }) => {
const handleChange = (event: NativeSyntheticEvent<SegmentedControlEvent>) => { const handleChange = useMemo(
onChange(event.nativeEvent.selectedIndex); () => (event: NativeSyntheticEvent<SegmentedControlEvent>) => {
}; if (event?.nativeEvent?.selectedIndex !== undefined) {
onChange(event.nativeEvent.selectedIndex);
}
},
[onChange],
);
if (!Array.isArray(values) || values.length === 0) {
return null;
}
return ( return (
<View style={styles.container}> <View style={styles.container}>

View file

@ -109,8 +109,8 @@ const SelectFeeModal = forwardRef<BottomModalHandle, SelectFeeModalProps>(
}); });
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
present: async () => feeModalRef.current?.present(), present: async () => await feeModalRef.current?.present(),
dismiss: async () => feeModalRef.current?.dismiss(), dismiss: async () => await feeModalRef.current?.dismiss(),
})); }));
const options: Option[] = [ const options: Option[] = [
@ -163,8 +163,8 @@ const SelectFeeModal = forwardRef<BottomModalHandle, SelectFeeModalProps>(
const handleSelectOption = async (fee: number | null, rate: number) => { const handleSelectOption = async (fee: number | null, rate: number) => {
setFeePrecalc(fp => ({ ...fp, current: fee })); setFeePrecalc(fp => ({ ...fp, current: fee }));
await feeModalRef.current?.dismiss();
setCustomFee(rate.toString()); setCustomFee(rate.toString());
await feeModalRef.current?.dismiss();
}; };
return ( return (
@ -340,14 +340,13 @@ const styles = StyleSheet.create({
feeModalFooter: { feeModalFooter: {
paddingVertical: 46, paddingVertical: 46,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
alignContent: 'center', alignContent: 'center',
alignItems: 'center', alignItems: 'center',
minHeight: 80, minHeight: 80,
}, },
feeModalFooterSpacing: { feeModalFooterSpacing: {
paddingHorizontal: 24, paddingHorizontal: 16,
}, },
memo: { memo: {
flexDirection: 'row', 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 React, { useCallback, useMemo } from 'react';
import { Platform, Pressable, TouchableOpacity } from 'react-native'; import { Platform, TouchableOpacity } from 'react-native';
import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu'; 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 { ToolTipMenuProps, Action } from './types';
import { useSettings } from '../hooks/context/useSettings'; import { useSettings } from '../hooks/context/useSettings';
const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => { const ToolTipMenu = (props: ToolTipMenuProps) => {
const { const {
title = '', title = '',
isMenuPrimaryAction = false, isMenuPrimaryAction = false,
renderPreview,
disabled = false, disabled = false,
onPress, onPress,
onMenuWillShow,
onMenuWillHide,
buttonStyle, buttonStyle,
onPressMenuItem, onPressMenuItem,
children, children,
@ -30,50 +19,59 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
const { language } = useSettings(); 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) // Map Menu Items for RN Menu (supports subactions and displayInline)
const mapMenuItemForMenuView = useCallback((action: Action): MenuAction | null => { const mapMenuItemForMenuView = useCallback((action: Action): MenuAction | null => {
if (!action.id) return null; if (!action.id) return null;
// Check for subactions // Check for subactions
const subactions = const subactions =
action.subactions?.map(subaction => ({ action.subactions?.map(subaction => {
id: subaction.id.toString(), const subMenuItem: MenuAction = {
title: subaction.text, id: subaction.id.toString(),
subtitle: subaction.subtitle, title: subaction.text,
image: subaction.icon?.iconValue ? subaction.icon.iconValue : undefined, subtitle: subaction.subtitle,
state: subaction.menuState === undefined ? undefined : ((subaction.menuState ? 'on' : 'off') as MenuState), image: subaction.icon?.iconValue ? subaction.icon.iconValue : undefined,
attributes: { disabled: subaction.disabled, destructive: subaction.destructive, hidden: subaction.hidden }, 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(), id: action.id.toString(),
title: action.text, title: action.text,
subtitle: action.subtitle, subtitle: action.subtitle,
image: action.icon?.iconValue ? action.icon.iconValue : undefined, 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 }, attributes: { disabled: action.disabled, destructive: action.destructive, hidden: action.hidden },
subactions: subactions.length > 0 ? subactions : undefined,
displayInline: action.displayInline || false, 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(() => { const menuViewItemsIOS = useMemo(() => {
return props.actions return props.actions
.map(actionGroup => { .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[]; return mergedActions.map(mapMenuItemForMenuView).filter(item => item !== null) as MenuAction[];
}, [props.actions, mapMenuItemForMenuView]); }, [props.actions, mapMenuItemForMenuView]);
const handlePressMenuItemForContextMenuView = useCallback(
(event: OnPressMenuItemEventObject) => {
onPressMenuItem(event.nativeEvent.actionKey);
},
[onPressMenuItem],
);
const handlePressMenuItemForMenuView = useCallback( const handlePressMenuItemForMenuView = useCallback(
({ nativeEvent }: NativeActionEvent) => { ({ nativeEvent }: NativeActionEvent) => {
onPressMenuItem(nativeEvent.event); onPressMenuItem(nativeEvent.event);
@ -114,46 +105,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
[onPressMenuItem], [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 = () => { const renderMenuView = () => {
return ( return (
<MenuView <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; 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 AsyncStorage from '@react-native-async-storage/async-storage';
import Clipboard from '@react-native-clipboard/clipboard'; import Clipboard from '@react-native-clipboard/clipboard';
import { Linking, View, ViewStyle } from 'react-native'; import { Linking, View, ViewStyle } from 'react-native';
@ -36,7 +36,7 @@ interface TransactionListItemProps {
type NavigationProps = NativeStackNavigationProp<DetailViewStackParamList>; 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 }) => { ({ item, itemPriceUnit = BitcoinUnit.BTC, walletID, searchQuery, style, renderHighlightedText }) => {
const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1); const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1);
const { colors } = useTheme(); const { colors } = useTheme();
@ -46,10 +46,10 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
const { language, selectedBlockExplorer } = useSettings(); const { language, selectedBlockExplorer } = useSettings();
const containerStyle = useMemo( const containerStyle = useMemo(
() => ({ () => ({
backgroundColor: 'transparent', backgroundColor: colors.background,
borderBottomColor: colors.lightBorder, borderBottomColor: colors.lightBorder,
}), }),
[colors.lightBorder], [colors.background, colors.lightBorder],
); );
const combinedStyle = useMemo(() => [containerStyle, style], [containerStyle, style]); const combinedStyle = useMemo(() => [containerStyle, style], [containerStyle, style]);
@ -81,28 +81,23 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
return sub || undefined; return sub || undefined;
}, [txMemo, item.confirmations, item.memo]); }, [txMemo, item.confirmations, item.memo]);
const formattedAmount = useMemo(() => {
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
}, [item.value, itemPriceUnit]);
const rowTitle = useMemo(() => { const rowTitle = useMemo(() => {
if (item.type === 'user_invoice' || item.type === 'payment_request') { if (item.type === 'user_invoice' || item.type === 'payment_request') {
if (isNaN(Number(item.value))) {
item.value = 0;
}
const currentDate = new Date(); 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!; const invoiceExpiration = item.timestamp! + item.expire_time!;
if (invoiceExpiration > now || item.ispaid) {
if (invoiceExpiration > now) { return formattedAmount;
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
} else { } else {
if (item.ispaid) { return loc.lnd.expired;
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
} else {
return loc.lnd.expired;
}
} }
} else {
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
} }
}, [item, itemPriceUnit]); return formattedAmount;
}, [item, formattedAmount]);
const rowTitleStyle = useMemo(() => { const rowTitleStyle = useMemo(() => {
let color = colors.successColor; let color = colors.successColor;
@ -164,6 +159,11 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
label: loc.transactions.expired_transaction, label: loc.transactions.expired_transaction,
icon: <TransactionExpiredIcon />, icon: <TransactionExpiredIcon />,
}; };
} else if (!item.ispaid) {
return {
label: loc.transactions.expired_transaction,
icon: <TransactionPendingIcon />,
};
} else { } else {
return { return {
label: loc.transactions.incoming_transaction, label: loc.transactions.incoming_transaction,
@ -193,10 +193,9 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
const { label: transactionTypeLabel, icon: avatar } = determineTransactionTypeAndAvatar(); const { label: transactionTypeLabel, icon: avatar } = determineTransactionTypeAndAvatar();
const amountWithUnit = useMemo(() => { const amountWithUnit = useMemo(() => {
const amount = formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); const unitSuffix = itemPriceUnit === BitcoinUnit.BTC || itemPriceUnit === BitcoinUnit.SATS ? ` ${itemPriceUnit}` : ' ';
const unit = itemPriceUnit === BitcoinUnit.BTC || itemPriceUnit === BitcoinUnit.SATS ? ` ${itemPriceUnit}` : ' '; return `${formattedAmount}${unitSuffix}`;
return `${amount}${unit}`; }, [formattedAmount, itemPriceUnit]);
}, [item.value, itemPriceUnit]);
useEffect(() => { useEffect(() => {
setSubtitleNumberOfLines(1); setSubtitleNumberOfLines(1);
@ -221,7 +220,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
} }
const loaded = await LN.loadSuccessfulPayment(paymentHash); const loaded = await LN.loadSuccessfulPayment(paymentHash);
if (loaded) { if (loaded) {
navigate('ScanLndInvoiceRoot', { navigate('ScanLNDInvoiceRoot', {
screen: 'LnurlPaySuccess', screen: 'LnurlPaySuccess',
params: { params: {
paymentHash, paymentHash,
@ -247,7 +246,19 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
setSubtitleNumberOfLines(0); 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 handleOnCopyAmountTap = useCallback(() => Clipboard.setString(rowTitle.replace(/[\s\\-]/g, '')), [rowTitle]);
const handleOnCopyTransactionID = useCallback(() => Clipboard.setString(item.hash), [item.hash]); const handleOnCopyTransactionID = useCallback(() => Clipboard.setString(item.hash), [item.hash]);
@ -278,6 +289,8 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
handleCopyOpenInBlockExplorerPress(); handleCopyOpenInBlockExplorerPress();
} else if (id === CommonToolTipActions.CopyTXID.id) { } else if (id === CommonToolTipActions.CopyTXID.id) {
handleOnCopyTransactionID(); handleOnCopyTransactionID();
} else if (id === CommonToolTipActions.Details.id) {
handleOnDetailsPress();
} }
}, },
[ [
@ -285,31 +298,40 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
handleOnCopyAmountTap, handleOnCopyAmountTap,
handleOnCopyNote, handleOnCopyNote,
handleOnCopyTransactionID, handleOnCopyTransactionID,
handleOnDetailsPress,
handleOnExpandNote, handleOnExpandNote,
handleOnViewOnBlockExplorer, handleOnViewOnBlockExplorer,
], ],
); );
const toolTipActions = useMemo((): Action[] => { const toolTipActions = useMemo((): Action[] => {
const actions: (Action | Action[])[] = []; const actions: (Action | Action[])[] = [
{
if (rowTitle !== loc.lnd.expired) { ...CommonToolTipActions.CopyAmount,
actions.push(CommonToolTipActions.CopyAmount); hidden: rowTitle === loc.lnd.expired,
} },
{
if (subtitle) { ...CommonToolTipActions.CopyNote,
actions.push(CommonToolTipActions.CopyNote); hidden: !subtitle,
} },
{
if (item.hash) { ...CommonToolTipActions.CopyTXID,
actions.push(CommonToolTipActions.CopyTXID, CommonToolTipActions.CopyBlockExplorerLink, [CommonToolTipActions.OpenInBlockExplorer]); hidden: !item.hash,
} },
{
if (subtitle && subtitleNumberOfLines === 1) { ...CommonToolTipActions.CopyBlockExplorerLink,
actions.push([CommonToolTipActions.ExpandNote]); hidden: !item.hash,
} },
[{ ...CommonToolTipActions.OpenInBlockExplorer, hidden: !item.hash }, CommonToolTipActions.Details],
[
{
...CommonToolTipActions.ExpandNote,
hidden: subtitleNumberOfLines !== 1,
},
],
];
return actions as Action[]; return actions as Action[];
}, [item.hash, subtitle, rowTitle, subtitleNumberOfLines]); }, [rowTitle, subtitle, item.hash, subtitleNumberOfLines]);
const accessibilityState = useMemo(() => { const accessibilityState = useMemo(() => {
return { return {
@ -317,6 +339,8 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
}; };
}, [subtitleNumberOfLines]); }, [subtitleNumberOfLines]);
const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]);
return ( return (
<ToolTipMenu <ToolTipMenu
isButton isButton
@ -338,8 +362,17 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
rightTitle={rowTitle} rightTitle={rowTitle}
rightTitleStyle={rowTitleStyle} rightTitleStyle={rowTitleStyle}
containerStyle={combinedStyle} containerStyle={combinedStyle}
testID="TransactionListItem"
/> />
</ToolTipMenu> </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> = ({ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps> = ({
wallet: initialWallet, wallet,
onWalletUnitChange, onWalletUnitChange,
onManageFundsPressed, onManageFundsPressed,
onWalletBalanceVisibilityChange, onWalletBalanceVisibilityChange,
unit = BitcoinUnit.BTC, unit = BitcoinUnit.BTC,
}) => { }) => {
const [wallet, setWallet] = useState(initialWallet); const { hideBalance } = wallet;
const [allowOnchainAddress, setAllowOnchainAddress] = useState(false); const [allowOnchainAddress, setAllowOnchainAddress] = useState(false);
const { preferredFiatCurrency } = useSettings(); const { preferredFiatCurrency } = useSettings();
@ -44,10 +44,6 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
} }
}, [wallet]); }, [wallet]);
useEffect(() => {
setWallet(initialWallet);
}, [initialWallet]);
useEffect(() => { useEffect(() => {
verifyIfWalletAllowsOnchainAddress(); verifyIfWalletAllowsOnchainAddress();
}, [wallet, verifyIfWalletAllowsOnchainAddress]); }, [wallet, verifyIfWalletAllowsOnchainAddress]);
@ -60,8 +56,8 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
}, [unit, wallet]); }, [unit, wallet]);
const handleBalanceVisibility = useCallback(() => { const handleBalanceVisibility = useCallback(() => {
onWalletBalanceVisibilityChange?.(!wallet.hideBalance); onWalletBalanceVisibilityChange?.(!hideBalance);
}, [onWalletBalanceVisibilityChange, wallet.hideBalance]); }, [onWalletBalanceVisibilityChange, hideBalance]);
const changeWalletBalanceUnit = () => { const changeWalletBalanceUnit = () => {
let newWalletPreferredUnit = wallet.getPreferredBalanceUnit(); let newWalletPreferredUnit = wallet.getPreferredBalanceUnit();
@ -112,17 +108,17 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
]; ];
}, []); }, []);
const balance = useMemo(() => { const currentBalance = wallet ? wallet.getBalance() : 0;
const hideBalance = wallet.hideBalance; const formattedBalance = useMemo(() => {
const balanceFormatted = return unit === BitcoinUnit.LOCAL_CURRENCY
unit === BitcoinUnit.LOCAL_CURRENCY ? formatBalance(currentBalance, unit, true)
? formatBalance(wallet.getBalance(), unit, true) : formatBalanceWithoutSuffix(currentBalance, unit, true);
: formatBalanceWithoutSuffix(wallet.getBalance(), unit, true); }, [unit, currentBalance]);
return !hideBalance && balanceFormatted;
}, [unit, wallet]); const balance = !wallet.hideBalance && formattedBalance;
const toolTipWalletBalanceActions = useMemo(() => { const toolTipWalletBalanceActions = useMemo(() => {
return wallet.hideBalance return hideBalance
? [ ? [
{ {
id: 'walletBalanceVisibility', id: 'walletBalanceVisibility',
@ -148,7 +144,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
}, },
}, },
]; ];
}, [wallet.hideBalance]); }, [hideBalance]);
const imageSource = useMemo(() => { const imageSource = useMemo(() => {
switch (wallet.type) { switch (wallet.type) {
@ -162,7 +158,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
}, [wallet.type]); }, [wallet.type]);
useAnimateOnChange(balance); useAnimateOnChange(balance);
useAnimateOnChange(wallet.hideBalance); useAnimateOnChange(hideBalance);
useAnimateOnChange(unit); useAnimateOnChange(unit);
useAnimateOnChange(wallet.getID?.()); useAnimateOnChange(wallet.getID?.());
@ -187,7 +183,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
actions={toolTipWalletBalanceActions} actions={toolTipWalletBalanceActions}
> >
<View style={styles.walletBalance}> <View style={styles.walletBalance}>
{wallet.hideBalance ? ( {hideBalance ? (
<BlurredBalanceView /> <BlurredBalanceView />
) : ( ) : (
<View> <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 { import {
Animated, Animated,
FlatList, FlatList,
@ -98,8 +98,12 @@ interface WalletCarouselItemProps {
isSelectedWallet?: boolean; isSelectedWallet?: boolean;
customStyle?: ViewStyle; customStyle?: ViewStyle;
horizontal?: boolean; horizontal?: boolean;
isPlaceHolder?: boolean;
searchQuery?: string; searchQuery?: string;
renderHighlightedText?: (text: string, query: string) => JSX.Element; renderHighlightedText?: (text: string, query: string) => JSX.Element;
animationsEnabled?: boolean;
onPressIn?: () => void;
onPressOut?: () => void;
} }
const iStyles = StyleSheet.create({ 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( export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
({ ({
item, item,
@ -186,6 +177,8 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
renderHighlightedText, renderHighlightedText,
animationsEnabled = true, animationsEnabled = true,
isPlaceHolder = false, isPlaceHolder = false,
onPressIn,
onPressOut,
}) => { }) => {
const scaleValue = useRef(new Animated.Value(1.0)).current; const scaleValue = useRef(new Animated.Value(1.0)).current;
const { colors } = useTheme(); 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 itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
const { isLargeScreen } = useIsLargeScreen(); 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(() => { const onPressedIn = useCallback(() => {
if (animationsEnabled) { if (animationsEnabled) {
Animated.spring(scaleValue, { animateScale(0.95);
toValue: 0.95,
useNativeDriver: true,
friction: 3,
tension: 100,
}).start();
} }
}, [scaleValue, animationsEnabled]); if (onPressIn) onPressIn();
}, [animateScale, animationsEnabled, onPressIn]);
const onPressedOut = useCallback(() => { const onPressedOut = useCallback(() => {
if (animationsEnabled) { if (animationsEnabled) {
Animated.spring(scaleValue, { animateScale(1.0);
toValue: 1.0,
useNativeDriver: true,
friction: 3,
tension: 100,
}).start();
} }
}, [scaleValue, animationsEnabled]); if (onPressOut) onPressOut();
}, [animateScale, animationsEnabled, onPressOut]);
const handlePress = useCallback(() => { const handlePress = useCallback(() => {
onPressedOut();
onPress(item); onPress(item);
}, [item, onPress, onPressedOut]); }, [item, onPress]);
const opacity = isSelectedWallet === false ? 0.5 : 1.0; const opacity = isSelectedWallet === false ? 0.5 : 1.0;
let image; let image;
@ -261,6 +253,8 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
if (handleLongPress) handleLongPress(); if (handleLongPress) handleLongPress();
}} }}
onPress={handlePress} onPress={handlePress}
delayHoverIn={0}
delayHoverOut={0}
> >
<View style={[iStyles.shadowContainer, { backgroundColor: colors.background, shadowColor: colors.shadowColor }]}> <View style={[iStyles.shadowContainer, { backgroundColor: colors.background, shadowColor: colors.shadowColor }]}>
<LinearGradient colors={WalletGradient.gradientsFor(item.type)} style={iStyles.grad}> <LinearGradient colors={WalletGradient.gradientsFor(item.type)} style={iStyles.grad}>
@ -356,6 +350,10 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
renderHighlightedText, renderHighlightedText,
isFlatList = true, isFlatList = true,
} = props; } = props;
const { width } = useWindowDimensions();
const itemWidth = React.useMemo(() => (width * 0.82 > 375 ? 375 : width * 0.82), [width]);
const renderItem = useCallback( const renderItem = useCallback(
({ item, index }: ListRenderItemInfo<TWallet>) => ({ item, index }: ListRenderItemInfo<TWallet>) =>
item ? ( item ? (
@ -373,7 +371,6 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
); );
const flatListRef = useRef<FlatList<any>>(null); const flatListRef = useRef<FlatList<any>>(null);
useImperativeHandle(ref, (): any => { useImperativeHandle(ref, (): any => {
return { return {
scrollToEnd: (params: { animated?: boolean | null | undefined } | undefined) => flatListRef.current?.scrollToEnd(params), 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(), getNativeScrollRef: () => flatListRef.current?.getNativeScrollRef(),
}; };
}, []); }, []);
const onScrollToIndexFailed = (error: { averageItemLength: number; index: number }): void => { const onScrollToIndexFailed = (error: { averageItemLength: number; index: number }): void => {
console.debug('onScrollToIndexFailed'); console.debug('onScrollToIndexFailed', error);
console.debug(error);
flatListRef.current?.scrollToOffset({ offset: error.averageItemLength * error.index, animated: true }); flatListRef.current?.scrollToOffset({ offset: error.averageItemLength * error.index, animated: true });
setTimeout(() => { setTimeout(() => {
if (data.length !== 0 && flatListRef.current !== null) { if (data.length !== 0 && flatListRef.current !== null) {
@ -407,16 +402,16 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
}, 100); }, 100);
}; };
const { width } = useWindowDimensions();
const sliderHeight = 195; 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 ? ( return isFlatList ? (
<FlatList <FlatList
ref={flatListRef} ref={flatListRef}
renderItem={renderItem} renderItem={renderItem}
extraData={data} extraData={data}
keyExtractor={(_, index) => index.toString()} keyExtractor={keyExtractor}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
pagingEnabled={horizontal} pagingEnabled={horizontal}
disableIntervalMomentum={horizontal} disableIntervalMomentum={horizontal}
@ -427,6 +422,7 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
initialNumToRender={10} initialNumToRender={10}
scrollEnabled={scrollEnabled} scrollEnabled={scrollEnabled}
keyboardShouldPersistTaps="handled"
ListHeaderComponent={ListHeaderComponent} ListHeaderComponent={ListHeaderComponent}
style={{ minHeight: sliderHeight + 12 }} style={{ minHeight: sliderHeight + 12 }}
onScrollToIndexFailed={onScrollToIndexFailed} onScrollToIndexFailed={onScrollToIndexFailed}

View file

@ -40,9 +40,7 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
borderBottomColor: colors.lightBorder, borderBottomColor: colors.lightBorder,
backgroundColor: colors.elevated, backgroundColor: colors.elevated,
}, },
list: {
color: colors.buttonTextColor,
},
index: { index: {
color: colors.alternativeTextColor, color: colors.alternativeTextColor,
}, },
@ -151,24 +149,29 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
title={item.address} title={item.address}
actions={menuActions} actions={menuActions}
onPressMenuItem={onToolTipPress} onPressMenuItem={onToolTipPress}
// Revisit once RNMenu has renderPreview prop
renderPreview={renderPreview} renderPreview={renderPreview}
onPress={navigateToReceive} onPress={navigateToReceive}
isButton isButton
> >
<ListItem key={item.key} containerStyle={stylesHook.container}> <ListItem key={item.key} containerStyle={stylesHook.container}>
<ListItem.Content style={stylesHook.list}> <ListItem.Content>
<ListItem.Title style={stylesHook.list} numberOfLines={1} ellipsizeMode="middle"> <View style={styles.row}>
<Text style={[styles.index, stylesHook.index]}>{item.index + 1}</Text>{' '} <View style={styles.leftSection}>
<Text style={[stylesHook.address, styles.address]}>{item.address}</Text> <Text style={[styles.index, stylesHook.index]}>{item.index}</Text>
</ListItem.Title> </View>
<View style={styles.subtitle}> <View style={styles.middleSection}>
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>{balance}</Text> <Text style={[stylesHook.address, styles.address]} numberOfLines={1} ellipsizeMode="middle">
{item.address}
</Text>
<Text style={[stylesHook.balance, styles.balance]}>{balance}</Text>
</View>
</View> </View>
</ListItem.Content> </ListItem.Content>
<View> <View style={styles.rightContainer}>
<AddressTypeBadge isInternal={item.isInternal} hasTransactions={hasTransactions} /> <AddressTypeBadge isInternal={item.isInternal} hasTransactions={hasTransactions} />
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}> <Text style={[stylesHook.balance, styles.balance]}>
{loc.addresses.transactions}: {item.transactions} {loc.addresses.transactions}: {item.transactions ?? 0}
</Text> </Text>
</View> </View>
</ListItem> </ListItem>
@ -179,20 +182,27 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
const styles = StyleSheet.create({ const styles = StyleSheet.create({
address: { address: {
fontWeight: 'bold', fontWeight: 'bold',
marginHorizontal: 40, marginHorizontal: 4,
}, },
index: { index: {
fontSize: 15, fontSize: 15,
}, },
balance: { balance: {
marginTop: 8, marginTop: 4,
marginLeft: 14,
}, },
subtitle: { row: {
flex: 1,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', alignItems: 'center',
width: '100%', },
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 { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import React from 'react'; 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 loc from '../loc';
import { Theme } from './themes'; import { Theme } from './themes';
@ -59,7 +59,6 @@ const navigationStyle = (
{ {
closeButtonPosition, closeButtonPosition,
onCloseButtonPressed, onCloseButtonPressed,
headerBackVisible = true,
...opts ...opts
}: NativeStackNavigationOptions & { }: NativeStackNavigationOptions & {
closeButtonPosition?: CloseButtonPosition; closeButtonPosition?: CloseButtonPosition;
@ -78,11 +77,6 @@ const navigationStyle = (
let headerRight; let headerRight;
let headerLeft; let headerLeft;
if (!headerBackVisible) {
headerLeft = () => <></>;
opts.headerLeft = headerLeft;
}
if (closeButton === CloseButtonPosition.Right) { if (closeButton === CloseButtonPosition.Right) {
headerRight = () => ( headerRight = () => (
<TouchableOpacity <TouchableOpacity
@ -108,17 +102,24 @@ const navigationStyle = (
</TouchableOpacity> </TouchableOpacity>
); );
} }
const baseHeaderStyle = {
let options: NativeStackNavigationOptions = {
headerShadowVisible: false, headerShadowVisible: false,
headerTitleStyle: { headerTitleStyle: {
fontWeight: '600', fontWeight: '600' as const,
color: theme.colors.foregroundColor, color: theme.colors.foregroundColor,
}, },
headerBackTitleVisible: false,
headerTintColor: theme.colors.foregroundColor, 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, headerRight,
headerLeft,
...opts, ...opts,
}; };

View file

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

View file

@ -41,37 +41,37 @@ platform :android do
Dir.chdir(project_root) do Dir.chdir(project_root) do
build_number = ENV['BUILD_NUMBER'] build_number = ENV['BUILD_NUMBER']
UI.user_error!("BUILD_NUMBER environment variable is missing") if build_number.nil? UI.user_error!("BUILD_NUMBER environment variable is missing") if build_number.nil?
# Extract versionName from build.gradle # Extract versionName from build.gradle
version_name = sh("grep versionName android/app/build.gradle | awk '{print $2}' | tr -d '\"'").strip 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 # Update versionCode in build.gradle
UI.message("Updating versionCode in build.gradle to #{build_number}...") UI.message("Updating versionCode in build.gradle to #{build_number}...")
build_gradle_path = "android/app/build.gradle" build_gradle_path = "android/app/build.gradle"
build_gradle_contents = File.read(build_gradle_path) build_gradle_contents = File.read(build_gradle_path)
new_build_gradle_contents = build_gradle_contents.gsub(/versionCode\s+\d+/, "versionCode #{build_number}") new_build_gradle_contents = build_gradle_contents.gsub(/versionCode\s+\d+/, "versionCode #{build_number}")
File.write(build_gradle_path, new_build_gradle_contents) File.write(build_gradle_path, new_build_gradle_contents)
# Determine branch name # Determine branch name and sanitize it
branch_name = ENV['GITHUB_HEAD_REF'] || `git rev-parse --abbrev-ref HEAD`.strip.gsub(/[\/\\:?*"<>|]/, '_') 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? 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 # Define APK name based on branch
signed_apk_name = branch_name != 'master' ? signed_apk_name = branch_name != 'master' ?
"BlueWallet-#{version_name}-#{build_number}-#{branch_name}".gsub(/[\/\\:?*"<>|]/, '_') + ".apk" : "BlueWallet-#{version_name}-#{build_number}-#{branch_name}.apk" :
"BlueWallet-#{version_name}-#{build_number}.apk" "BlueWallet-#{version_name}-#{build_number}.apk"
# Define paths # Define paths
unsigned_apk_path = "android/app/build/outputs/apk/release/app-release-unsigned.apk" 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}" 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 # Rename APK
if File.exist?(unsigned_apk_path) if File.exist?(unsigned_apk_path)
UI.message("Renaming APK to #{signed_apk_name}...") 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}") UI.error("Unsigned APK not found at path: #{unsigned_apk_path}")
next next
end end
# Sign APK # Sign APK
UI.message("Signing APK with apksigner...") 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}") 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}") UI.message("APK signed successfully: #{signed_apk_path}")
end end
end end
end
desc "Upload APK to BrowserStack and post result as PR comment" desc "Upload APK to BrowserStack and post result as PR comment"
lane :upload_to_browserstack_and_comment do lane :upload_to_browserstack_and_comment do
@ -126,16 +128,18 @@ platform :android do
You can test it on the following devices: 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 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 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)](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 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 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 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 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)](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 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 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.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 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) - [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}) **Filename**: [#{apk_filename}](#{apk_download_url})
**BrowserStack App URL**: #{app_url} **BrowserStack App URL**: #{app_url}
COMMENT COMMENT
@ -188,6 +192,40 @@ end
# =========================== # ===========================
platform :ios do 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" desc "Register new devices from a file"
lane :register_devices_from_txt do lane :register_devices_from_txt do
@ -234,26 +272,33 @@ platform :ios do
desc "Synchronize certificates and provisioning profiles" desc "Synchronize certificates and provisioning profiles"
lane :setup_provisioning_profiles do 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...") UI.message("Setting up provisioning profiles...")
platform = "ios"
# Iterate over app identifiers to fetch provisioning profiles # Iterate over app identifiers to fetch provisioning profiles
app_identifiers.each do |app_identifier| app_identifiers.each do |app_identifier|
match( with_retry(3, "Fetching provisioning profile for #{app_identifier}") do
git_basic_authorization: ENV["GIT_ACCESS_TOKEN"], UI.message("Fetching provisioning profile for #{app_identifier}...")
git_url: ENV["GIT_URL"], match(
type: "appstore", git_basic_authorization: ENV["GIT_ACCESS_TOKEN"],
clone_branch_directly: true, # Skip if the branch already exists git_url: ENV["GIT_URL"],
platform: platform, type: "appstore",
app_identifier: app_identifier, clone_branch_directly: true,
team_id: ENV["ITC_TEAM_ID"], platform: "ios",
team_name: ENV["ITC_TEAM_NAME"], app_identifier: app_identifier,
readonly: true, team_id: ENV["ITC_TEAM_ID"],
keychain_name: "temp_keychain", team_name: ENV["ITC_TEAM_NAME"],
keychain_password: ENV["KEYCHAIN_PASSWORD"] readonly: true,
) keychain_name: "temp_keychain",
keychain_password: ENV["KEYCHAIN_PASSWORD"]
)
log_success("Successfully fetched provisioning profile for #{app_identifier}")
end
end end
log_success("All provisioning profiles set up")
end end
desc "Fetch development certificates and provisioning profiles for Mac Catalyst" desc "Fetch development certificates and provisioning profiles for Mac Catalyst"
@ -398,48 +443,114 @@ lane :upload_bugsnag_sourcemaps do
end end
desc "Build the iOS app" desc "Build the iOS app"
lane :build_app_lane do lane :build_app_lane do
Dir.chdir(project_root) do Dir.chdir(project_root) do
UI.message("Building the application from: #{Dir.pwd}") UI.message("Building the application from: #{Dir.pwd}")
workspace_path = File.join(project_root, "ios", "BlueWallet.xcworkspace") workspace_path = File.join(project_root, "ios", "BlueWallet.xcworkspace")
export_options_path = File.join(project_root, "ios", "export_options.plist") export_options_path = File.join(project_root, "ios", "export_options.plist")
clear_derived_data_lane clear_derived_data_lane
begin # Determine which iOS version to use
build_ios_app( ios_version = determine_ios_version
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
# Use File.join to construct paths without extra slashes UI.message("Using iOS version: #{ios_version}")
ipa_path = lane_context[SharedValues::IPA_OUTPUT_PATH] 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) # Check for IPA path from both our defined path and fastlane's context
UI.message("IPA successfully found at: #{ipa_path}") 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 ENV['IPA_OUTPUT_PATH'] = ipa_path
sh("echo 'IPA_OUTPUT_PATH=#{ipa_path}' >> $GITHUB_ENV") # Export for GitHub Actions # Set both standard output format and the newer GITHUB_OUTPUT format
else sh("echo 'ipa_output_path=#{ipa_path}' >> $GITHUB_OUTPUT") if ENV['GITHUB_OUTPUT']
UI.user_error!("IPA not found after build_ios_app.") 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 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 end
# =========================== # ===========================
# Global Lanes # Global Lanes
@ -589,5 +700,4 @@ lane :update_release_notes do |options|
UI.error("No localization found for locale #{locale}") UI.error("No localization found for locale #{locale}")
end end
end end
end
end end

View file

@ -3,33 +3,39 @@
# URL of the Git repository to store the certificates # URL of the Git repository to store the certificates
git_url(ENV["GIT_URL"]) git_url(ENV["GIT_URL"])
# Define the type of match to run, could be one of 'appstore', 'adhoc', 'development', or 'enterprise'. # Define the type of match to run
# For example, use 'appstore' for App Store builds, 'adhoc' for Ad Hoc distribution, # Default to "appstore" but can be overridden
# 'development' for development builds, and 'enterprise' for In-House (enterprise) distribution. type(ENV["MATCH_TYPE"] || "appstore")
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. # Your Apple Developer account email address
# Replace with your app's bundle identifier(s).
# Your Apple Developer account email address.
username(ENV["APPLE_ID"]) 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"]) team_id(ENV["ITC_TEAM_ID"])
# Set this to true if match should only read existing certificates and profiles # Set readonly based on environment (default to true for safety)
# and not create new ones. # Set to false explicitly when new profiles need to be created
readonly(true) readonly(ENV["MATCH_READONLY"] == "false" ? false : true)
# Optional: The Git branch that is used for match. # Define the platform to use
# 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'.
platform("ios") 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-browserstack'
gem 'fastlane-plugin-bugsnag_sourcemaps_upload' 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 { Platform } from 'react-native';
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions'; import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { navigationRef } from '../NavigationService'; import { navigationRef } from '../NavigationService.ts';
/**
* 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,
});
});
});
}
const isCameraAuthorizationStatusGranted = async () => { const isCameraAuthorizationStatusGranted = async () => {
const status = await check(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA); const status = await check(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA);
@ -59,4 +11,18 @@ const requestCameraAuthorization = () => {
return request(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA); 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.navigate('ScanQRCode', {
showFileImportButton: true,
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 { CommonActions } from '@react-navigation/native';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { AppState, AppStateStatus, Linking } from 'react-native'; import { AppState, AppStateStatus, Linking } from 'react-native';
@ -21,25 +19,42 @@ import loc from '../loc';
import { Chain } from '../models/bitcoinUnits'; import { Chain } from '../models/bitcoinUnits';
import { navigationRef } from '../NavigationService'; import { navigationRef } from '../NavigationService';
import ActionSheet from '../screen/ActionSheet'; import ActionSheet from '../screen/ActionSheet';
import { useStorage } from '../hooks/context/useStorage'; import { useStorage } from './context/useStorage';
import RNQRGenerator from 'rn-qr-generator'; import RNQRGenerator from 'rn-qr-generator';
import presentAlert from './Alert'; import presentAlert from '../components/Alert';
import useMenuElements from '../hooks/useMenuElements'; import useWidgetCommunication from './useWidgetCommunication';
import useWidgetCommunication from '../hooks/useWidgetCommunication'; import useWatchConnectivity from './useWatchConnectivity';
import useWatchConnectivity from '../hooks/useWatchConnectivity'; import useDeviceQuickActions from './useDeviceQuickActions';
import useDeviceQuickActions from '../hooks/useDeviceQuickActions'; import useHandoffListener from './useHandoffListener';
import useHandoffListener from '../hooks/useHandoffListener'; import useMenuElements from './useMenuElements';
const ClipboardContentType = Object.freeze({ const ClipboardContentType = Object.freeze({
BITCOIN: 'BITCOIN', BITCOIN: 'BITCOIN',
LIGHTNING: 'LIGHTNING', 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 appState = useRef<AppStateStatus>(AppState.currentState);
const clipboardContent = useRef<undefined | string>(); 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(); useWatchConnectivity();
useWidgetCommunication(); useWidgetCommunication();
useMenuElements(); useMenuElements();
@ -47,6 +62,8 @@ const CompanionDelegates = () => {
useHandoffListener(); useHandoffListener();
const processPushNotifications = useCallback(async () => { const processPushNotifications = useCallback(async () => {
if (!shouldActivateListeners) return false;
await new Promise(resolve => setTimeout(resolve, 200)); await new Promise(resolve => setTimeout(resolve, 200));
try { try {
const notifications2process = await getStoredNotifications(); const notifications2process = await getStoredNotifications();
@ -166,45 +183,58 @@ const CompanionDelegates = () => {
console.error('Failed to process push notifications:', error); console.error('Failed to process push notifications:', error);
} }
return false; return false;
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets]); }, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets, shouldActivateListeners]);
useEffect(() => { useEffect(() => {
if (!shouldActivateListeners) return;
initializeNotifications(processPushNotifications); initializeNotifications(processPushNotifications);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [shouldActivateListeners]);
const handleOpenURL = useCallback( const handleOpenURL = useCallback(
async (event: { url: string }): Promise<void> => { async (event: { url: string }): Promise<void> => {
const { url } = event; if (!shouldActivateListeners) return;
if (url) { try {
const decodedUrl = decodeURIComponent(url); if (!event.url) return;
const fileName = decodedUrl.split('/').pop()?.toLowerCase(); let decodedUrl: string;
try {
if (fileName && /\.(jpe?g|png)$/i.test(fileName)) { 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 { try {
const values = await RNQRGenerator.detect({ qrResult = await RNQRGenerator.detect({ uri: decodedUrl });
uri: decodedUrl, } catch (e) {
}); console.error('QR detection first attempt failed:', e);
}
if (values && values.values.length > 0) { if (!qrResult || !qrResult.values || qrResult.values.length === 0) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); const altUrl = decodedUrl.replace(/^file:\/\//, '');
DeeplinkSchemaMatch.navigationRouteFor( try {
{ url: values.values[0] }, qrResult = await RNQRGenerator.detect({ uri: altUrl });
(value: [string, any]) => navigationRef.navigate(...value), } catch (e) {
{ console.error('QR detection second attempt failed:', e);
wallets,
addWallet,
saveToDisk,
setSharedCosigner,
},
);
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.send.qr_error_no_qrcode });
} }
} 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 { } else {
DeeplinkSchemaMatch.navigationRouteFor(event, (value: [string, any]) => navigationRef.navigate(...value), { DeeplinkSchemaMatch.navigationRouteFor(event, (value: [string, any]) => navigationRef.navigate(...value), {
@ -214,12 +244,19 @@ const CompanionDelegates = () => {
setSharedCosigner, 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( const showClipboardAlert = useCallback(
({ contentType }: { contentType: undefined | string }) => { ({ contentType }: { contentType: undefined | string }) => {
if (!shouldActivateListeners) return;
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight); triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
getClipboardContent().then(clipboard => { getClipboardContent().then(clipboard => {
if (!clipboard) return; if (!clipboard) return;
@ -242,12 +279,13 @@ const CompanionDelegates = () => {
); );
}); });
}, },
[handleOpenURL], [handleOpenURL, shouldActivateListeners],
); );
const handleAppStateChange = useCallback( const handleAppStateChange = useCallback(
async (nextAppState: AppStateStatus | undefined) => { async (nextAppState: AppStateStatus | undefined) => {
if (wallets.length === 0) return; if (!shouldActivateListeners || wallets.length === 0) return;
if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) { if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) {
setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000); setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000);
updateExchangeRate(); updateExchangeRate();
@ -287,10 +325,12 @@ const CompanionDelegates = () => {
appState.current = nextAppState; appState.current = nextAppState;
} }
}, },
[processPushNotifications, showClipboardAlert, wallets], [processPushNotifications, showClipboardAlert, wallets, shouldActivateListeners],
); );
const addListeners = useCallback(() => { const addListeners = useCallback(() => {
if (!shouldActivateListeners) return { urlSubscription: null, appStateSubscription: null };
const urlSubscription = Linking.addEventListener('url', handleOpenURL); const urlSubscription = Linking.addEventListener('url', handleOpenURL);
const appStateSubscription = AppState.addEventListener('change', handleAppStateChange); const appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
@ -298,18 +338,16 @@ const CompanionDelegates = () => {
urlSubscription, urlSubscription,
appStateSubscription, appStateSubscription,
}; };
}, [handleOpenURL, handleAppStateChange]); }, [handleOpenURL, handleAppStateChange, shouldActivateListeners]);
useEffect(() => { useEffect(() => {
const subscriptions = addListeners(); const subscriptions = addListeners();
return () => { return () => {
subscriptions.urlSubscription?.remove(); subscriptions.urlSubscription?.remove?.();
subscriptions.appStateSubscription?.remove(); subscriptions.appStateSubscription?.remove?.();
}; };
}, [addListeners]); }, [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'; 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); const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => { useEffect(() => {
const handler = debounce((val: T) => { if (!isFn) {
setDebouncedValue(val); const handler = setTimeout(() => setDebouncedValue(value), delay);
}, delay); return () => clearTimeout(handler);
}
}, [isFn, value, delay]);
handler(value); return isFn ? (debouncedFunction as unknown as T) : debouncedValue;
}
return () => {
handler.cancel();
};
}, [value, delay]);
return debouncedValue;
};
export default useDebounce; export default useDebounce;

View file

@ -3,9 +3,16 @@ import { navigationRef } from '../NavigationService';
import { presentWalletExportReminder } from '../helpers/presentWalletExportReminder'; import { presentWalletExportReminder } from '../helpers/presentWalletExportReminder';
import { unlockWithBiometrics, useBiometrics } from './useBiometrics'; import { unlockWithBiometrics, useBiometrics } from './useBiometrics';
import { useStorage } from './context/useStorage'; import { useStorage } from './context/useStorage';
import { requestCameraAuthorization } from '../helpers/scan-qr';
import { useCallback, useMemo } from 'react';
// List of screens that require biometrics // 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 // List of screens that require wallet export to be saved
const requiresWalletExportIsSaved = ['ReceiveDetailsRoot', 'WalletAddresses']; const requiresWalletExportIsSaved = ['ReceiveDetailsRoot', 'WalletAddresses'];
@ -15,96 +22,125 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
const { wallets, saveToDisk } = useStorage(); const { wallets, saveToDisk } = useStorage();
const { isBiometricUseEnabled } = useBiometrics(); const { isBiometricUseEnabled } = useBiometrics();
const enhancedNavigate: NavigationProp<ParamListBase>['navigate'] = ( const enhancedNavigate = useCallback(
screenOrOptions: any, (
params?: any, ...args:
options?: { merge?: boolean }, | [string]
) => { | [string, object | undefined]
let screenName: string; | [string, object | undefined, { merge?: boolean }]
if (typeof screenOrOptions === 'string') { | [{ name: string; params?: object; path?: string; merge?: boolean }]
screenName = screenOrOptions; ) => {
} else if (typeof screenOrOptions === 'object' && 'name' in screenOrOptions) { let screenOrOptions: any;
screenName = screenOrOptions.name; let params: any;
params = screenOrOptions.params; // Assign params from object if present let options: { merge?: boolean } | undefined;
} else {
throw new Error('Invalid navigation options');
}
const isRequiresBiometrics = requiresBiometrics.includes(screenName); if (typeof args[0] === 'string') {
const isRequiresWalletExportIsSaved = requiresWalletExportIsSaved.includes(screenName); screenOrOptions = args[0];
params = args[1];
const proceedWithNavigation = () => { options = args[2];
console.log('Proceeding with navigation to', screenName); } else {
if (navigationRef.current?.isReady()) { screenOrOptions = args[0];
if (typeof screenOrOptions === 'string') { }
originalNavigation.navigate({ name: screenOrOptions, params, merge: options?.merge }); let screenName: string;
} else { if (typeof screenOrOptions === 'string') {
originalNavigation.navigate({ ...screenOrOptions, params, merge: options?.merge }); 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 () => { const isRequiresBiometrics = requiresBiometrics.includes(screenName);
if (isRequiresBiometrics) { const isRequiresWalletExportIsSaved = requiresWalletExportIsSaved.includes(screenName);
const isBiometricsEnabled = await isBiometricUseEnabled();
if (isBiometricsEnabled) { const proceedWithNavigation = () => {
const isAuthenticated = await unlockWithBiometrics(); console.log('Proceeding with navigation to', screenName);
if (isAuthenticated) { if (navigationRef.current?.isReady()) {
proceedWithNavigation(); if (typeof screenOrOptions === 'string') {
return; originalNavigation.navigate({ name: screenOrOptions, params, merge: options?.merge });
} else { } else {
console.error('Biometric authentication failed'); originalNavigation.navigate({ ...screenOrOptions, params, merge: options?.merge });
// Decide if navigation should proceed or not after failed authentication
return; // Prevent proceeding with the original navigation if bio fails
} }
} }
} };
if (isRequiresWalletExportIsSaved) {
console.log('Checking if wallet export is saved'); (async () => {
let walletID: string | undefined; // NEW: If the current (active) screen is 'ScanQRCode', bypass all checks.
if (params && params.walletID) { const currentRouteName = navigationRef.current?.getCurrentRoute()?.name;
walletID = params.walletID; if (currentRouteName === 'ScanQRCode') {
} else if (params && params.params && params.params.walletID) {
walletID = params.params.walletID;
}
if (!walletID) {
proceedWithNavigation(); proceedWithNavigation();
return; return;
} }
const wallet = wallets.find(w => w.getID() === walletID);
if (wallet && !wallet.getUserHasSavedExport()) { if (isRequiresBiometrics) {
try { const isBiometricsEnabled = await isBiometricUseEnabled();
await presentWalletExportReminder(); if (isBiometricsEnabled) {
wallet.setUserHasSavedExport(true); const isAuthenticated = await unlockWithBiometrics();
await saveToDisk(); // Assuming saveToDisk() returns a Promise. 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(); proceedWithNavigation();
} catch (error) { return;
if (error) { }
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', { originalNavigation.navigate('WalletExportRoot', {
screen: 'WalletExport', screen: 'WalletExport',
params: { walletID }, 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('WalletsList');
} }, [enhancedNavigate]);
return { return useMemo(
...originalNavigation, () => ({
navigate: enhancedNavigate, ...originalNavigation,
navigateToWalletsList, navigate: enhancedNavigate,
}; navigateToWalletsList,
}),
[originalNavigation, enhancedNavigate, navigateToWalletsList],
);
}; };
// Usage example: // Usage example:
// type NavigationProps = NativeStackNavigationProp<SendDetailsStackParamList, 'SendDetails'>; // type NavigationProps = NativeStackNavigationProp<SendDetailsStackParamList, 'SendDetails'>;
// const navigation = useExtendedNavigation<NavigationProps>(); // const navigation = useExtendedNavigation<NavigationProps>();

View file

@ -23,20 +23,25 @@ const useHandoffListener = () => {
const handleUserActivity = useCallback( const handleUserActivity = useCallback(
(data: UserActivityData) => { (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 { activityType, userInfo } = data;
const modifiedUserInfo = { ...(userInfo || {}), type: activityType };
try { try {
if (activityType === HandOffActivityType.ReceiveOnchain) { if (activityType === HandOffActivityType.ReceiveOnchain && modifiedUserInfo.address) {
navigate('ReceiveDetailsRoot', { navigate('ReceiveDetailsRoot', {
screen: 'ReceiveDetails', 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', { navigate('WalletXpubRoot', {
screen: 'WalletXpub', screen: 'WalletXpub',
params: { xpub: userInfo.xpub }, params: { xpub: modifiedUserInfo.xpub, type: activityType },
}); });
} else { } else {
console.debug(`Unhandled activity type: ${activityType}`); console.debug(`Unhandled or incomplete activity type/data: ${activityType}`, modifiedUserInfo);
} }
} catch (error) { } catch (error) {
console.error('Error handling user activity:', error); console.error('Error handling user activity:', error);
@ -50,9 +55,13 @@ const useHandoffListener = () => {
const activitySubscription = eventEmitter?.addListener('onUserActivityOpen', handleUserActivity); const activitySubscription = eventEmitter?.addListener('onUserActivityOpen', handleUserActivity);
EventEmitter.getMostRecentUserActivity?.() if (EventEmitter && EventEmitter.getMostRecentUserActivity) {
.then(handleUserActivity) EventEmitter.getMostRecentUserActivity()
.catch(() => console.debug('No userActivity object sent')); .then(handleUserActivity)
.catch(() => console.debug('No valid user activity object received'));
} else {
console.debug('EventEmitter native module is not available.');
}
return () => { return () => {
activitySubscription?.remove(); 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 { NativeEventEmitter, NativeModules, Platform } from 'react-native';
import { navigationRef } from '../NavigationService';
import { CommonActions } from '@react-navigation/native'; 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. 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 { MenuElementsEmitter } = NativeModules;
const eventEmitter = let eventEmitter: NativeEventEmitter | null = null;
(Platform.OS === 'ios' || Platform.OS === 'macos') && MenuElementsEmitter ? new NativeEventEmitter(MenuElementsEmitter) : null; let listenersInitialized = false;
const useMenuElements = () => { // Registry for transaction handlers by screen ID
const { walletsInitialized } = useStorage(); const handlerRegistry = new Map<string, MenuActionHandler>();
const reloadTransactionsMenuActionRef = useRef<() => void>(() => {});
const setReloadTransactionsMenuActionFunction = useCallback((newFunction: () => void) => { // Store subscription references for proper cleanup
console.debug('Setting reloadTransactionsMenuActionFunction.'); let subscriptions: { remove: () => void }[] = [];
reloadTransactionsMenuActionRef.current = newFunction;
}, []);
const dispatchNavigate = useCallback((routeName: string, screen?: string) => { // Create a more robust emitter with error handling
NavigationService.dispatch(CommonActions.navigate({ name: routeName, params: screen ? { screen } : undefined })); 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( /**
() => ({ * Safely navigate using multiple fallback approaches
openSettings: () => dispatchNavigate('Settings'), */
addWallet: () => dispatchNavigate('AddWalletRoot'), function safeNavigate(routeName: string, params?: Record<string, any>): void {
importWallet: () => dispatchNavigate('AddWalletRoot', 'ImportWallet'), try {
reloadTransactions: () => { if (navigationRef.current?.isReady()) {
console.debug('Calling reloadTransactionsMenuActionFunction'); navigationRef.current.navigate(routeName as never, params as never);
reloadTransactionsMenuActionRef.current?.(); return;
}, }
}),
[dispatchNavigate],
);
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(() => { useEffect(() => {
if (!walletsInitialized || !eventEmitter) return; initializeListeners();
console.debug('Setting up menu event listeners'); const unsubscribe = navigationRef.addListener('state', () => {});
// 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);
return () => { return () => {
console.debug('Removing reloadTransactionsMenuAction listener'); unsubscribe();
reloadTransactionsListener.remove();
}; };
}, [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 { return {
setReloadTransactionsMenuActionFunction, registerTransactionsHandler,
unregisterTransactionsHandler,
isMenuElementsSupported: !!eventEmitter,
}; };
}; };

View file

@ -1,8 +1,28 @@
const useMenuElements = () => { import { useCallback } from 'react';
const setReloadTransactionsMenuActionFunction = (_: () => void) => {};
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 { 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 './shim.js';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
@ -12,7 +13,12 @@ if (!Error.captureStackTrace) {
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 = () => { const BlueAppComponent = () => {
useEffect(() => { useEffect(() => {

View file

@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 60; objectVersion = 63;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@ -14,7 +14,6 @@
32F0A29A2311DBB20095C559 /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F0A2992311DBB20095C559 /* ComplicationController.swift */; }; 32F0A29A2311DBB20095C559 /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F0A2992311DBB20095C559 /* ComplicationController.swift */; };
6D2A6464258BA92D0092292B /* Stickers.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6D2A6463258BA92D0092292B /* Stickers.xcassets */; }; 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, ); }; }; 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 */; }; 6D4AF15925D21172009DD853 /* MarketAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D9A2E6A254BAB1B007B5B82 /* MarketAPI.swift */; };
6D4AF16D25D21192009DD853 /* Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEB4BFA254FBA0E00E9F9AA /* Placeholders.swift */; }; 6D4AF16D25D21192009DD853 /* Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEB4BFA254FBA0E00E9F9AA /* Placeholders.swift */; };
6D4AF17825D211A3009DD853 /* FiatUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D2AA8072568B8F40090B089 /* FiatUnit.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 */; }; 782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B9D9B3A7B2CB4255876B67AF /* libz.tbd */; };
849047CA2702A32A008EE567 /* Handoff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849047C92702A32A008EE567 /* Handoff.swift */; }; 849047CA2702A32A008EE567 /* Handoff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849047C92702A32A008EE567 /* Handoff.swift */; };
84E05A842721191B001A0D3A /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 84E05A832721191B001A0D3A /* Settings.bundle */; }; 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 */; }; B40D4E34225841EC00428FCC /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E32225841EC00428FCC /* Interface.storyboard */; };
B40D4E36225841ED00428FCC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; }; 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, ); }; }; 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 */; }; B440340F2BCC40A400162242 /* fiatUnits.json in Resources */ = {isa = PBXBuildFile; fileRef = B440340E2BCC40A400162242 /* fiatUnits.json */; };
B44034102BCC40A400162242 /* 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 */; }; 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 */; }; B450109C2C0FCD8A00619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
B450109D2C0FCD9F00619044 /* 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 */; }; 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 */; }; 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 */; }; B461B852299599F800E431AA /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = B461B851299599F800E431AA /* AppDelegate.mm */; };
B4742E972CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; }; B4742E972CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
B4742E982CCDBE8300380EEE /* 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 */; }; B4AB225E2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; };
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; }; B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; };
B4B1A4642BFA73110072E3BB /* 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 */; }; B4D0B2622C1DEA11006B6B1B /* ReceivePageInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2612C1DEA11006B6B1B /* ReceivePageInterfaceController.swift */; };
B4D0B2642C1DEA99006B6B1B /* ReceiveType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2632C1DEA99006B6B1B /* ReceiveType.swift */; }; B4D0B2642C1DEA99006B6B1B /* ReceiveType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2632C1DEA99006B6B1B /* ReceiveType.swift */; };
B4D0B2662C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.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>"; }; 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>"; }; 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>"; }; 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; }; 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; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSegmentedControl.swift; sourceTree = "<group>"; };
B4C075292CDDB3C500322A84 /* MenuElementsEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MenuElementsEmitter.m; 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>"; }; 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>"; }; 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>"; }; B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveInterfaceMode.swift; sourceTree = "<group>"; };
@ -489,7 +490,6 @@
13B07FAE1A68108700A75B9A /* BlueWallet */ = { 13B07FAE1A68108700A75B9A /* BlueWallet */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
B4C0752B2CDDB3CC00322A84 /* MenuElementsEmitter */,
B461B850299599F800E431AA /* AppDelegate.h */, B461B850299599F800E431AA /* AppDelegate.h */,
B461B851299599F800E431AA /* AppDelegate.mm */, B461B851299599F800E431AA /* AppDelegate.mm */,
32C7944323B8879D00BE2AFA /* BlueWalletRelease.entitlements */, 32C7944323B8879D00BE2AFA /* BlueWalletRelease.entitlements */,
@ -501,8 +501,6 @@
32B5A3292334450100F8D608 /* Bridge.swift */, 32B5A3292334450100F8D608 /* Bridge.swift */,
32B5A3282334450100F8D608 /* BlueWallet-Bridging-Header.h */, 32B5A3282334450100F8D608 /* BlueWallet-Bridging-Header.h */,
6DF25A9E249DB97E001D06F5 /* LaunchScreen.storyboard */, 6DF25A9E249DB97E001D06F5 /* LaunchScreen.storyboard */,
6D32C5C42596CE2F008C077C /* EventEmitter.h */,
6D32C5C52596CE3A008C077C /* EventEmitter.m */,
84E05A832721191B001A0D3A /* Settings.bundle */, 84E05A832721191B001A0D3A /* Settings.bundle */,
B4742E962CCDBE8300380EEE /* Localizable.xcstrings */, B4742E962CCDBE8300380EEE /* Localizable.xcstrings */,
); );
@ -677,6 +675,15 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
B409AB072D71E07C00BA06F8 /* MenuElementsEmitter */ = {
isa = PBXGroup;
children = (
B409AB052D71E07500BA06F8 /* MenuElementsEmitter.swift */,
B409AB032D71DFAA00BA06F8 /* MenuElementsEmitter.m */,
);
path = MenuElementsEmitter;
sourceTree = "<group>";
};
B40D4E31225841EC00428FCC /* BlueWalletWatch */ = { B40D4E31225841EC00428FCC /* BlueWalletWatch */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -786,6 +793,15 @@
path = Shared; path = Shared;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
B44305BD2D6A04B9004675CC /* SegmentedControl */ = {
isa = PBXGroup;
children = (
B44305BB2D6A04B2004675CC /* CustomSegmentedControl.m */,
B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */,
);
path = SegmentedControl;
sourceTree = "<group>";
};
B450109A2C0FCD7E00619044 /* Utilities */ = { B450109A2C0FCD7E00619044 /* Utilities */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -798,21 +814,14 @@
B45010A12C1504E900619044 /* Components */ = { B45010A12C1504E900619044 /* Components */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
B409AB072D71E07C00BA06F8 /* MenuElementsEmitter */,
B44305BD2D6A04B9004675CC /* SegmentedControl */,
B4B3EC232D69FF8700327F3D /* EventEmitter.swift */,
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */, B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */,
B45010A82C1507F000619044 /* SegmentedControl */,
); );
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
B45010A82C1507F000619044 /* SegmentedControl */ = {
isa = PBXGroup;
children = (
B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */,
B45010A92C15080500619044 /* CustomSegmentedControlManager.h */,
);
path = SegmentedControl;
sourceTree = "<group>";
};
B4549F2E2B80FEA1002E3153 /* ci_scripts */ = { B4549F2E2B80FEA1002E3153 /* ci_scripts */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -829,15 +838,6 @@
path = BlueWalletUITests; path = BlueWalletUITests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
B4C0752B2CDDB3CC00322A84 /* MenuElementsEmitter */ = {
isa = PBXGroup;
children = (
B4C075292CDDB3C500322A84 /* MenuElementsEmitter.m */,
B4C075282CDDB3BE00322A84 /* MenuElementsEmitter.h */,
);
path = MenuElementsEmitter;
sourceTree = "<group>";
};
FAA856B639C61E61D2CF90A8 /* Pods */ = { FAA856B639C61E61D2CF90A8 /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -993,7 +993,7 @@
}; };
}; };
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "BlueWallet" */; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "BlueWallet" */;
compatibilityVersion = "Xcode 15.0"; compatibilityVersion = "Xcode 15.3";
developmentRegion = en_US; developmentRegion = en_US;
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
knownRegions = ( knownRegions = (
@ -1239,28 +1239,30 @@
B44033EE2BCC374500162242 /* Numeric+abbreviated.swift in Sources */, B44033EE2BCC374500162242 /* Numeric+abbreviated.swift in Sources */,
B48630E82CCEE92400A8425C /* PriceWidget.swift in Sources */, B48630E82CCEE92400A8425C /* PriceWidget.swift in Sources */,
B44033DD2BCC36C300162242 /* LatestTransaction.swift in Sources */, B44033DD2BCC36C300162242 /* LatestTransaction.swift in Sources */,
6D32C5C62596CE3A008C077C /* EventEmitter.m in Sources */,
B49A28C12CD199FC006B08E4 /* SwiftTCPClient.swift in Sources */, B49A28C12CD199FC006B08E4 /* SwiftTCPClient.swift in Sources */,
B44033FE2BCC37D700162242 /* MarketAPI.swift in Sources */, B44033FE2BCC37D700162242 /* MarketAPI.swift in Sources */,
B450109C2C0FCD8A00619044 /* Utilities.swift in Sources */, B450109C2C0FCD8A00619044 /* Utilities.swift in Sources */,
B409AB042D71DFAA00BA06F8 /* MenuElementsEmitter.m in Sources */,
B48630E52CCEE8B800A8425C /* PriceView.swift in Sources */, B48630E52CCEE8B800A8425C /* PriceView.swift in Sources */,
B48630E72CCEE91900A8425C /* PriceWidgetProvider.swift in Sources */, B48630E72CCEE91900A8425C /* PriceWidgetProvider.swift in Sources */,
B4B3EC252D69FF8700327F3D /* EventEmitter.swift in Sources */,
B49A28C02CD199C7006B08E4 /* MarketAPI+Electrum.swift in Sources */, B49A28C02CD199C7006B08E4 /* MarketAPI+Electrum.swift in Sources */,
B48630ED2CCEEEB000A8425C /* WalletAppShortcuts.swift in Sources */, B48630ED2CCEEEB000A8425C /* WalletAppShortcuts.swift in Sources */,
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */, B409AB062D71E07500BA06F8 /* MenuElementsEmitter.swift in Sources */,
B44033CE2BCC352900162242 /* UserDefaultsGroup.swift in Sources */, B44033CE2BCC352900162242 /* UserDefaultsGroup.swift in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */,
B45942C42CDECF2400B3DC2E /* MenuElementsEmitter.m in Sources */,
B461B852299599F800E431AA /* AppDelegate.mm in Sources */, B461B852299599F800E431AA /* AppDelegate.mm in Sources */,
B44033F42BCC377F00162242 /* WidgetData.swift in Sources */, B44033F42BCC377F00162242 /* WidgetData.swift in Sources */,
B49A28C52CD1A894006B08E4 /* MarketData.swift in Sources */, B49A28C52CD1A894006B08E4 /* MarketData.swift in Sources */,
B49A28BF2CD18A9A006B08E4 /* FiatUnitEnum.swift in Sources */, B49A28BF2CD18A9A006B08E4 /* FiatUnitEnum.swift in Sources */,
B44305BC2D6A04B2004675CC /* CustomSegmentedControl.m in Sources */,
B44033C42BCC332400162242 /* Balance.swift in Sources */, B44033C42BCC332400162242 /* Balance.swift in Sources */,
B48630EE2CCEEEE900A8425C /* PriceIntent.swift in Sources */, B48630EE2CCEEEE900A8425C /* PriceIntent.swift in Sources */,
B44034072BCC38A000162242 /* FiatUnit.swift in Sources */, B44034072BCC38A000162242 /* FiatUnit.swift in Sources */,
B44034002BCC37F800162242 /* Bundle+decode.swift in Sources */, B44034002BCC37F800162242 /* Bundle+decode.swift in Sources */,
B44033E22BCC36CB00162242 /* Placeholders.swift in Sources */, B44033E22BCC36CB00162242 /* Placeholders.swift in Sources */,
B4793DBB2CEDACBD00C92C2E /* Chain.swift in Sources */, B4793DBB2CEDACBD00C92C2E /* Chain.swift in Sources */,
B4B3EC222D69FF6C00327F3D /* CustomSegmentedControl.swift in Sources */,
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */, B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */,
B48630E12CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */, B48630E12CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */,
B44033DA2BCC369A00162242 /* Colors.swift in Sources */, B44033DA2BCC369A00162242 /* Colors.swift in Sources */,
@ -1453,7 +1455,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703136799; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
@ -1471,7 +1473,7 @@
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate"; INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate";
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -1481,7 +1483,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.0.6; MARKETING_VERSION = 7.1.5;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
@ -1516,7 +1518,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703136799; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
@ -1529,7 +1531,7 @@
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate"; INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate";
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -1539,7 +1541,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.0.6; MARKETING_VERSION = 7.1.5;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
@ -1575,20 +1577,20 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703136799; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = Stickers/Info.plist; INFOPLIST_FILE = Stickers/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(SDKROOT)/usr/lib/swift", "$(SDKROOT)/usr/lib/swift",
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.0.6; MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1618,20 +1620,20 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703136799; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = Stickers/Info.plist; INFOPLIST_FILE = Stickers/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(SDKROOT)/usr/lib/swift", "$(SDKROOT)/usr/lib/swift",
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.0.6; MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers;
@ -1662,7 +1664,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703136799; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
@ -1670,7 +1672,7 @@
"DEVELOPMENT_TEAM[sdk=macosx*]" = A7W54YZ4WU; "DEVELOPMENT_TEAM[sdk=macosx*]" = A7W54YZ4WU;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = Widgets/Info.plist; INFOPLIST_FILE = Widgets/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.6; IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -1681,7 +1683,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.0.6; MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1718,7 +1720,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703136799; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
@ -1726,7 +1728,7 @@
"DEVELOPMENT_TEAM[sdk=macosx*]" = A7W54YZ4WU; "DEVELOPMENT_TEAM[sdk=macosx*]" = A7W54YZ4WU;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = Widgets/Info.plist; INFOPLIST_FILE = Widgets/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.6; IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -1737,7 +1739,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.0.6; MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget;
@ -1905,7 +1907,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703136799; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
@ -1925,7 +1927,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.0.6; MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -1958,7 +1960,7 @@
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703136799; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
@ -1978,7 +1980,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.0.6; MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension;
@ -2010,7 +2012,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1703136799; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
@ -2024,7 +2026,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.0.6; MARKETING_VERSION = 7.1.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
@ -2059,7 +2061,7 @@
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1703136799; CURRENT_PROJECT_VERSION = 1703169999;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
@ -2073,7 +2075,7 @@
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
"$(inherited)", "$(inherited)",
); );
MARKETING_VERSION = 7.0.6; MARKETING_VERSION = 7.1.5;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch;

View file

@ -6,12 +6,9 @@
#import "RNQuickActionManager.h" #import "RNQuickActionManager.h"
#import <UserNotifications/UserNotifications.h> #import <UserNotifications/UserNotifications.h>
#import <RNCPushNotificationIOS.h> #import <RNCPushNotificationIOS.h>
#import "EventEmitter.h"
#import "MenuElementsEmitter.h"
#import <React/RCTRootView.h> #import <React/RCTRootView.h>
#import <Bugsnag/Bugsnag.h> #import <Bugsnag/Bugsnag.h>
#import "BlueWallet-Swift.h" #import "BlueWallet-Swift.h"
#import "CustomSegmentedControlManager.h"
@interface AppDelegate() <UNUserNotificationCenterDelegate> @interface AppDelegate() <UNUserNotificationCenterDelegate>
@ -23,8 +20,6 @@
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{ {
[MenuElementsEmitter sharedInstance];
[CustomSegmentedControlManager registerIfNecessary];
[self clearFilesIfNeeded]; [self clearFilesIfNeeded];
self.userDefaultsGroup = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"]; self.userDefaultsGroup = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"];
@ -154,27 +149,42 @@
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity
restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler 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"]; [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"] || if ([userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.receiveonchain"] ||
[userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.xpub"] || [userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.xpub"] ||
[userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.blockexplorer"]) { [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; return YES;
} }
if (userActivity.activityType == NSUserActivityTypeBrowsingWeb) { // Forward web browsing activities to LinkingManager
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
return [RCTLinkingManager application:application return [RCTLinkingManager application:application
continueUserActivity:userActivity continueUserActivity:userActivity
restorationHandler:restorationHandler]; 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; return NO;
} }
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options { - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [RCTLinkingManager application:app openURL:url options: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 -(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{ {
NSDictionary *userInfo = notification.request.content.userInfo; NSDictionary *userInfo = notification.request.content.userInfo;
completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge); completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionBadge);
} }
- (void)buildMenuWithBuilder:(id<UIMenuBuilder>)builder { - (void)buildMenuWithBuilder:(id<UIMenuBuilder>)builder {
@ -244,25 +254,59 @@
} }
- (void)openSettings:(UIKeyCommand *)keyCommand { - (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 { - (void)addWalletAction:(UIKeyCommand *)keyCommand {
// Implement the functionality for adding a wallet // Safely access the MenuElementsEmitter
[MenuElementsEmitter.sharedInstance addWalletMenuAction]; MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
NSLog(@"Add Wallet action performed"); 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 { - (void)importWalletAction:(UIKeyCommand *)keyCommand {
// Implement the functionality for adding a wallet // Safely access the MenuElementsEmitter
[MenuElementsEmitter.sharedInstance importWalletMenuAction]; MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
NSLog(@"Import Wallet action performed"); 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 { - (void)reloadTransactionsAction:(UIKeyCommand *)keyCommand {
// Implement the functionality for adding a wallet // Safely access the MenuElementsEmitter
[MenuElementsEmitter.sharedInstance reloadTransactionsMenuAction]; MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
NSLog(@"Reload Transactions action performed"); 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 { - (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" "size" : "1024x1024"
}, },
{ {
"filename" : "16pt@1x.png", "filename" : "icon_16x16.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "1x", "scale" : "1x",
"size" : "16x16" "size" : "16x16"
}, },
{ {
"filename" : "16pt@2x.png", "filename" : "icon_16x16@2x.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "2x", "scale" : "2x",
"size" : "16x16" "size" : "16x16"
}, },
{ {
"filename" : "32pt@1x.png", "filename" : "icon_32x32.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "1x", "scale" : "1x",
"size" : "32x32" "size" : "32x32"
}, },
{ {
"filename" : "32pt@2x.png", "filename" : "icon_32x32@2x.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "2x", "scale" : "2x",
"size" : "32x32" "size" : "32x32"
}, },
{ {
"filename" : "128pt@1x.png", "filename" : "icon_128x128.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "1x", "scale" : "1x",
"size" : "128x128" "size" : "128x128"
}, },
{ {
"filename" : "128pt@2x.png", "filename" : "icon_128x128@2x.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "2x", "scale" : "2x",
"size" : "128x128" "size" : "128x128"
}, },
{ {
"filename" : "256pt@1x.png", "filename" : "icon_256x256.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "1x", "scale" : "1x",
"size" : "256x256" "size" : "256x256"
}, },
{ {
"filename" : "256pt@2x.png", "filename" : "icon_256x256@2x.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "2x", "scale" : "2x",
"size" : "256x256" "size" : "256x256"
}, },
{ {
"filename" : "512pt@1x.png", "filename" : "icon_512x512.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "1x", "scale" : "1x",
"size" : "512x512" "size" : "512x512"
}, },
{ {
"filename" : "512pt@2x.png", "filename" : "icon_512x512@2x.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "2x", "scale" : "2x",
"size" : "512x512" "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