mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-13 11:09:20 +01:00
Compare commits
453 commits
Author | SHA1 | Date | |
---|---|---|---|
|
5f18540ca7 | ||
|
751c7d6f45 | ||
|
0b1c3dd9f7 | ||
|
ae89a59794 | ||
|
10b3432e0e | ||
|
c67eea8155 | ||
|
9421511f74 | ||
|
9ec0ef51e4 | ||
|
1cada11c50 | ||
|
d2cebde6ad | ||
|
1a940971bc | ||
|
28316b4d73 | ||
|
4670eea38a | ||
|
b2552bdc71 | ||
|
dbd4066f7e | ||
|
4cdd952f90 | ||
|
ddee4cdaaf | ||
|
0aa6b96e4b | ||
|
8d49aff279 | ||
|
18a187b120 | ||
|
1f77a852a8 | ||
|
9d899d672d | ||
|
e7b81e5517 | ||
|
f8af06e2ae | ||
|
8b81472fa4 | ||
|
4ad2b15070 | ||
|
dd118af993 | ||
|
d375bd9780 | ||
|
c967f6701a | ||
|
4753d984ca | ||
|
28e2e343b8 | ||
|
b81879e9bd | ||
|
040f91028a | ||
|
1c8aa08de8 | ||
|
d7743a740f | ||
|
a2ec407720 | ||
|
c90bf68a66 | ||
|
7ab73669dc | ||
|
72134fef84 | ||
|
c0eec1be8e | ||
|
77f9cf7e16 | ||
|
88be0332e4 | ||
|
a4f8e42d8c | ||
|
e519360d89 | ||
|
62a5efc82c | ||
|
ec2bc5e627 | ||
|
0bdfc6fa85 | ||
|
ef5887f28b | ||
|
3f82cb4449 | ||
|
9574554780 | ||
|
4b249edaa4 | ||
|
e3fcbbb713 | ||
|
63a3c61534 | ||
|
a6d66574cd | ||
|
2376ef8be9 | ||
|
155f021692 | ||
|
3a26d6dab2 | ||
|
1da481542a | ||
|
472307c271 | ||
|
09394ff4f9 | ||
|
16936fca27 | ||
|
54166c0592 | ||
|
bb6d443670 | ||
|
e4e16a8f40 | ||
|
35deca58e0 | ||
|
898443f3a5 | ||
|
15fc708a0a | ||
|
15c618b59a | ||
|
ccdb492ba0 | ||
|
0449965ef5 | ||
|
758c2acf3a | ||
|
b75aa7b269 | ||
|
aa695f2705 | ||
|
863ac46bc8 | ||
|
1946fa0dde | ||
|
a62a21b28b | ||
|
c1ae300254 | ||
|
5e4d58b207 | ||
|
3504d0dc30 | ||
|
fe795e648b | ||
|
af8d7d3477 | ||
|
c604ac4197 | ||
|
4c0fd89530 | ||
|
d14b4265f8 | ||
|
307e950d15 | ||
|
10f145d012 | ||
|
88b8274758 | ||
|
a65776933d | ||
|
05491387ff | ||
|
e23f233f25 | ||
|
7eb420c561 | ||
|
96e553f3d5 | ||
|
2f3cf1b4e9 | ||
|
6a4392de02 | ||
|
62bb33a9ff | ||
|
85cd7b4aed | ||
|
9507a48314 | ||
|
07b93d521d | ||
|
7c6bf01372 | ||
|
b01aa58e3b | ||
|
136dd20f9e | ||
|
0bfeda0d75 | ||
|
1a848328e3 | ||
|
93e6269611 | ||
|
5f8dbc52d1 | ||
|
e4d3ecba98 | ||
|
bbe4449dd9 | ||
|
bf9087eae6 | ||
|
b3ff1b7c3f | ||
|
00dcc25142 | ||
|
4614c51041 | ||
|
8d694ceb7b | ||
|
49f6068b21 | ||
|
be8437e107 | ||
|
cc71dfce8c | ||
|
2e1f20c080 | ||
|
11dceb19fa | ||
|
7bb3dd6aef | ||
|
7e7492d314 | ||
|
c4b1e67f9d | ||
|
79f624e906 | ||
|
fabfc5c156 | ||
|
a3d234bee1 | ||
|
850ac2c653 | ||
|
1c26cb420e | ||
|
0f23c4d0a7 | ||
|
9adef2b3c1 | ||
|
a9b003e762 | ||
|
37b03b12e7 | ||
|
4e7c5a28ae | ||
|
9766d2387a | ||
|
3939ef32f9 | ||
|
17a5a78fd8 | ||
|
f7d673d93b | ||
|
dc3e88c005 | ||
|
85cb6c1287 | ||
|
8488dfb9e7 | ||
|
3dde81f3a8 | ||
|
a0dc0a31e7 | ||
|
2dc26ac26a | ||
|
b881370f83 | ||
|
4f31aff503 | ||
|
2ee13dcf4f | ||
|
bf65d2d302 | ||
|
3599ef50ad | ||
|
80fb4a74a8 | ||
|
4f326452ab | ||
|
37a88fd60d | ||
|
3fcf3c0840 | ||
|
685332ce22 | ||
|
0892332d23 | ||
|
11cb7dbed5 | ||
|
7205f70c30 | ||
|
aa9e647c28 | ||
|
8439ff9893 | ||
|
f83476428e | ||
|
4be2668c81 | ||
|
867581003c | ||
|
3f1ea9432b | ||
|
2bcbe9903e | ||
|
7963083237 | ||
|
b7e03c1ed1 | ||
|
b74fb5f389 | ||
|
ddd141fc11 | ||
|
d9a70f5879 | ||
|
e645c911d7 | ||
|
06fbb8c945 | ||
|
c008a644cc | ||
|
ab7e7cf1d5 | ||
|
d38968086e | ||
|
787fcb797d | ||
|
599b5d3b60 | ||
|
c3f7d5b184 | ||
|
3423730a41 | ||
|
2bb7b0c53f | ||
|
b0a7053fc0 | ||
|
a8c0da4768 | ||
|
b357053e65 | ||
|
5b20ac352e | ||
|
8c2640e5a9 | ||
|
a6575b7b73 | ||
|
db58bcf70d | ||
|
b8d1d686f0 | ||
|
5d088a67c1 | ||
|
fa43d03a36 | ||
|
5a7fb86742 | ||
|
4ff759b538 | ||
|
0b33af59f0 | ||
|
f9d8594509 | ||
|
7f97c340f8 | ||
|
925dc17042 | ||
|
164f16657a | ||
|
34db010bde | ||
|
7ccf19212f | ||
|
133312e065 | ||
|
46a78e8dfb | ||
|
e5ab5f6565 | ||
|
ad71dccd72 | ||
|
159a8b2e16 | ||
|
d3fd8c050f | ||
|
52b3cb9b34 | ||
|
98b643a023 | ||
|
0eb3393f1f | ||
|
47a08448a2 | ||
|
254550d92e | ||
|
9ed7ceabdb | ||
|
6afc6624eb | ||
|
2317d0a4cf | ||
|
dcd2023815 | ||
|
4b37eaba98 | ||
|
8619e80dc0 | ||
|
e06d1ce57c | ||
|
157bd3529a | ||
|
e92eb7eae0 | ||
|
b9227cdbc6 | ||
|
a047c0219e | ||
|
2ca8eca810 | ||
|
93f901e94f | ||
|
670ad6a833 | ||
|
b232a13243 | ||
|
d610063809 | ||
|
1b328cd130 | ||
|
0b5e640630 | ||
|
527219f697 | ||
|
c400771d7a | ||
|
021ed454f1 | ||
|
47673a4ae0 | ||
|
7e66e42862 | ||
|
e25833f0d3 | ||
|
0e698069f4 | ||
|
3a8a7d6da8 | ||
|
1b561c8a91 | ||
|
4b93827b7f | ||
|
3de6976997 | ||
|
9c1be484c1 | ||
|
f04b50c58b | ||
|
f974658472 | ||
|
208157430f | ||
|
b73f04b4e6 | ||
|
0429721d66 | ||
|
5fb3991cb2 | ||
|
e4093a357d | ||
|
fda596211a | ||
|
fb1a30191d | ||
|
2f3ac6e972 | ||
|
8719ded414 | ||
|
680d9d4495 | ||
|
45f095badf | ||
|
66bb0b0e1c | ||
|
33acf30d68 | ||
|
2c4bb95475 | ||
|
9435fb769f | ||
|
6321627578 | ||
|
18cb2faef6 | ||
|
2c68583495 | ||
|
f8629e2555 | ||
|
6c11e2a5b8 | ||
|
827c2ad3db | ||
|
4be2bb03be | ||
|
12d8596180 | ||
|
2a4b14d63e | ||
|
062b8844d4 | ||
|
e4cea4f451 | ||
|
82f13fbded | ||
|
a1d5941a75 | ||
|
4d9a2f79f9 | ||
|
fef62f2fd8 | ||
|
c1adabb021 | ||
|
b42290ceee | ||
|
0aa2ed20f3 | ||
|
c092ea4523 | ||
|
d338f813cb | ||
|
238ee798ab | ||
|
bbf746b011 | ||
|
fdd2b66d8e | ||
|
44fc028159 | ||
|
632500b734 | ||
|
a4a513f703 | ||
|
39b141507c | ||
|
dde4520094 | ||
|
a7843e127f | ||
|
f4125cb1e9 | ||
|
115b0a2a4f | ||
|
7035bec229 | ||
|
80ef3252a1 | ||
|
6cd6079493 | ||
|
9a8158a384 | ||
|
9976734665 | ||
|
1c15ae0a0c | ||
|
64d8238872 | ||
|
336785e1a0 | ||
|
d68b806b60 | ||
|
1acf4c9af2 | ||
|
2d51238d6f | ||
|
3b90c49d79 | ||
|
226d499603 | ||
|
f155b6b577 | ||
|
4be7f78be8 | ||
|
5d81a4cf57 | ||
|
1b11200a0a | ||
|
f229beb5e0 | ||
|
e176783a3f | ||
|
5c7460d6b1 | ||
|
571b056854 | ||
|
54db4b366e | ||
|
02dd22b8d4 | ||
|
8b94a9db10 | ||
|
175a5f27aa | ||
|
907e54938c | ||
|
c43d36d84d | ||
|
f900f46deb | ||
|
472b6c97ff | ||
|
ae80cb9118 | ||
|
f389af09ba | ||
|
fc5eccfcd4 | ||
|
307306f5ec | ||
|
73081033ed | ||
|
8b531350af | ||
|
4c09a52e02 | ||
|
879f4f4081 | ||
|
ea870729d6 | ||
|
b8a8986a8e | ||
|
caf41400a0 | ||
|
30ed2a3d96 | ||
|
a8374ab25b | ||
|
d3fd15dcf8 | ||
|
e8c181359d | ||
|
307f6881c9 | ||
|
e38e70bb0b | ||
|
92fda5d969 | ||
|
e69c105ccf | ||
|
002efdc4e7 | ||
|
63ab4da34b | ||
|
c2ff24591e | ||
|
19dd1047d1 | ||
|
38d92a7a5c | ||
|
59f2835cb8 | ||
|
9863dfd47b | ||
|
70e32c9d69 | ||
|
2b393ba997 | ||
|
88a1ec4260 | ||
|
5eabded72b | ||
|
d957ee7197 | ||
|
a4df48a0c5 | ||
|
6d7e48eb1a | ||
|
0481c8d6a9 | ||
|
85e47ac83d | ||
|
0d64347813 | ||
|
4849042dc6 | ||
|
d2e186bbf7 | ||
|
588da24f0b | ||
|
a8858833ef | ||
|
06ec5feb4b | ||
|
e9c3e3143f | ||
|
83f545ed4b | ||
|
7b8b3a0be2 | ||
|
6dde0c4b4e | ||
|
e810baf9c8 | ||
|
facd7b7783 | ||
|
89e7b23c05 | ||
|
56c983e1dc | ||
|
016e9f4214 | ||
|
c932d0da5a | ||
|
cdd923db7c | ||
|
4611c46d1e | ||
|
59c9edeebd | ||
|
05eff5f2b4 | ||
|
8894bcf965 | ||
|
a6306c53d8 | ||
|
d05d51237a | ||
|
5c70faf17d | ||
|
18c5e38d6c | ||
|
ca1be7d443 | ||
|
ec027a12df | ||
|
6b013e5bb7 | ||
|
950848181e | ||
|
559468b221 | ||
|
3adb90abff | ||
|
6765dd7246 | ||
|
6698645f48 | ||
|
cdd76db18f | ||
|
715991b106 | ||
|
7882716c73 | ||
|
9f912c51ed | ||
|
8ae9ac6155 | ||
|
ea4acc2556 | ||
|
26c9449f2d | ||
|
b21cf6e0ec | ||
|
ce18286d45 | ||
|
d46d16140a | ||
|
5aef6382b9 | ||
|
16d418a6c1 | ||
|
4fc57adaac | ||
|
8c504a1bd1 | ||
|
e197fd70d4 | ||
|
342a127f99 | ||
|
a41032cfda | ||
|
0675e6ea62 | ||
|
19ba071af3 | ||
|
34b7525cba | ||
|
86c0d9d53d | ||
|
d8d97b2b39 | ||
|
2503cb7882 | ||
|
ca912377bc | ||
|
cea31518dc | ||
|
d09e8ff68c | ||
|
346581b3e2 | ||
|
10f7e44232 | ||
|
faf86028ab | ||
|
76c4023592 | ||
|
bd42acb7c7 | ||
|
3d9e9ddf88 | ||
|
0c88e0e9db | ||
|
0371e3352f | ||
|
4607d4a796 | ||
|
4a44989a8f | ||
|
0f6582e050 | ||
|
279afa517f | ||
|
e1202c6854 | ||
|
021a1fd352 | ||
|
d51010dd85 | ||
|
8f26859f76 | ||
|
49c5e67f45 | ||
|
a4a6fa5ef4 | ||
|
96b1331a60 | ||
|
791cbd5f94 | ||
|
073280225d | ||
|
077f3a3a04 | ||
|
1b73ab9b06 | ||
|
9acaac9646 | ||
|
19bddcb152 | ||
|
ddf00d5d44 | ||
|
721f0d3ecb | ||
|
6561bb0524 | ||
|
8b32825e73 | ||
|
2cdd01f2c2 | ||
|
fc7eb4ece2 | ||
|
486bc43202 | ||
|
fe37bcb9fd | ||
|
75a26d155c | ||
|
38ab7665bc | ||
|
c3ae3c8104 | ||
|
f6a6d7c41e | ||
|
cf16417e00 | ||
|
40c5cc7295 | ||
|
f1a7fc8c40 | ||
|
7a218a4fa6 | ||
|
5f2378a9de | ||
|
9583fac4c6 | ||
|
fac654c263 | ||
|
17eba2d925 | ||
|
2f4b688bfd | ||
|
0c43cc7b24 |
212 changed files with 9485 additions and 6405 deletions
|
@ -47,6 +47,24 @@
|
|||
"device": "emulator",
|
||||
"app": "android.debug"
|
||||
},
|
||||
"android.debug.device": {
|
||||
"device": {
|
||||
"device": {
|
||||
"adbName": ".*"
|
||||
},
|
||||
"type": "android.attached"
|
||||
},
|
||||
"app": "android.debug"
|
||||
},
|
||||
"android.release.device": {
|
||||
"device": {
|
||||
"device": {
|
||||
"adbName": ".*"
|
||||
},
|
||||
"type": "android.attached"
|
||||
},
|
||||
"app": "android.release"
|
||||
},
|
||||
"android.release": {
|
||||
"device": "emulator",
|
||||
"app": "android.release"
|
||||
|
|
124
.github/workflows/build-ios-release-pullrequest.yml
vendored
124
.github/workflows/build-ios-release-pullrequest.yml
vendored
|
@ -22,12 +22,40 @@ jobs:
|
|||
branch_name: ${{ steps.get_latest_commit_details.outputs.branch_name }}
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
MATCH_READONLY: "true"
|
||||
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Ensures the full Git history is available
|
||||
|
||||
- name: Setup Caching
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/CocoaPods
|
||||
ios/Pods
|
||||
~/.npm
|
||||
node_modules
|
||||
vendor/bundle
|
||||
key: ${{ runner.os }}-ios-${{ hashFiles('**/package-lock.json', '**/Podfile.lock', '**/Gemfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ios-
|
||||
|
||||
- name: Clear All Caches
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
echo "Clearing Xcode DerivedData..."
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData
|
||||
echo "Clearing CocoaPods Cache..."
|
||||
rm -rf ~/Library/Caches/CocoaPods
|
||||
echo "Clearing npm Cache..."
|
||||
npm cache clean --force
|
||||
echo "Clearing Ruby Gems Cache..."
|
||||
rm -rf ~/.gem
|
||||
echo "Clearing Bundler Cache..."
|
||||
rm -rf ~/.bundle/cache
|
||||
|
||||
- name: Ensure Correct Branch
|
||||
if: github.ref != 'refs/heads/master'
|
||||
|
@ -67,15 +95,32 @@ jobs:
|
|||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: 16.0
|
||||
xcode-version: latest
|
||||
|
||||
- name: Install iOS Simulator Runtime
|
||||
run: |
|
||||
echo "Available iOS simulator runtimes:"
|
||||
xcrun simctl list runtimes
|
||||
|
||||
# Try to download the latest iOS 16.x simulator if not present
|
||||
if (! xcrun simctl list runtimes | grep -q "iOS 16"); then
|
||||
echo "Installing iOS 16.4 simulator..."
|
||||
sudo xcode-select -s /Applications/Xcode.app
|
||||
xcodebuild -downloadPlatform iOS
|
||||
fi
|
||||
|
||||
echo "Available iOS simulator runtimes after install:"
|
||||
xcrun simctl list runtimes
|
||||
|
||||
- name: Set Up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.1.6
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install Dependencies with Bundler
|
||||
run: |
|
||||
|
@ -88,6 +133,7 @@ jobs:
|
|||
- name: Install CocoaPods Dependencies
|
||||
run: |
|
||||
bundle exec fastlane ios install_pods
|
||||
echo "CocoaPods dependencies installed successfully"
|
||||
|
||||
- name: Generate Build Number Based on Timestamp
|
||||
id: generate_build_number
|
||||
|
@ -133,8 +179,26 @@ jobs:
|
|||
- name: Build App
|
||||
id: build_app
|
||||
run: |
|
||||
bundle exec fastlane ios build_app_lane --verbose
|
||||
echo "ipa_output_path=$IPA_OUTPUT_PATH" >> $GITHUB_OUTPUT # Set the IPA output path for future jobs
|
||||
bundle exec fastlane ios build_app_lane
|
||||
|
||||
# Ensure IPA path is set for subsequent steps
|
||||
if [ -f "./ios/build/ipa_path.txt" ]; then
|
||||
IPA_PATH=$(cat ./ios/build/ipa_path.txt)
|
||||
echo "IPA_OUTPUT_PATH=$IPA_PATH" >> $GITHUB_ENV
|
||||
echo "ipa_output_path=$IPA_PATH" >> $GITHUB_OUTPUT
|
||||
echo "Found IPA at: $IPA_PATH"
|
||||
else
|
||||
echo "Warning: ipa_path.txt not found, trying to locate IPA file manually..."
|
||||
IPA_PATH=$(find ./ios -name "*.ipa" | head -n 1)
|
||||
if [ -n "$IPA_PATH" ]; then
|
||||
echo "IPA_OUTPUT_PATH=$IPA_PATH" >> $GITHUB_ENV
|
||||
echo "ipa_output_path=$IPA_PATH" >> $GITHUB_OUTPUT
|
||||
echo "Found IPA at: $IPA_PATH"
|
||||
else
|
||||
echo "Error: No IPA file found"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Upload Bugsnag Sourcemaps
|
||||
if: success()
|
||||
|
@ -142,8 +206,8 @@ jobs:
|
|||
env:
|
||||
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
|
||||
BUGSNAG_RELEASE_STAGE: production
|
||||
PROJECT_VERSION: ${{ needs.build.outputs.project_version }}
|
||||
NEW_BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }}
|
||||
PROJECT_VERSION: ${{ env.PROJECT_VERSION }}
|
||||
NEW_BUILD_NUMBER: ${{ env.NEW_BUILD_NUMBER }}
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
|
@ -151,13 +215,32 @@ jobs:
|
|||
with:
|
||||
name: build_logs
|
||||
path: ./ios/build_logs/
|
||||
retention-days: 7
|
||||
|
||||
- name: Verify IPA File Before Upload
|
||||
run: |
|
||||
echo "Checking IPA file at: $IPA_OUTPUT_PATH"
|
||||
if [ -f "$IPA_OUTPUT_PATH" ]; then
|
||||
echo "✅ IPA file exists"
|
||||
ls -la "$IPA_OUTPUT_PATH"
|
||||
else
|
||||
echo "❌ IPA file not found at: $IPA_OUTPUT_PATH"
|
||||
echo "Current directory contents:"
|
||||
find ./ios -name "*.ipa"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload IPA as Artifact
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: BlueWallet_${{env.PROJECT_VERSION}}_${{env.NEW_BUILD_NUMBER}}.ipa
|
||||
path: ${{ env.IPA_OUTPUT_PATH }} # Directly from Fastfile `IPA_OUTPUT_PATH`
|
||||
name: BlueWallet_IPA
|
||||
path: ${{ env.IPA_OUTPUT_PATH }}
|
||||
retention-days: 7
|
||||
|
||||
- name: Delete Temporary Keychain
|
||||
if: always()
|
||||
run: bundle exec fastlane ios delete_temp_keychain
|
||||
|
||||
testflight-upload:
|
||||
needs: build
|
||||
|
@ -177,6 +260,7 @@ jobs:
|
|||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.1.6
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install Dependencies with Bundler
|
||||
run: |
|
||||
|
@ -186,18 +270,11 @@ jobs:
|
|||
- name: Download IPA from Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: BlueWallet_${{ needs.build.outputs.project_version }}_${{ needs.build.outputs.new_build_number }}.ipa
|
||||
name: BlueWallet_IPA
|
||||
path: ./
|
||||
|
||||
- name: Create App Store Connect API Key JSON
|
||||
run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' > ./appstore_api_key.json
|
||||
|
||||
- name: Verify IPA File Download
|
||||
run: |
|
||||
echo "Current directory:"
|
||||
pwd
|
||||
echo "Files in current directory:"
|
||||
ls -la ./
|
||||
|
||||
- name: Set IPA Path Environment Variable
|
||||
run: echo "IPA_OUTPUT_PATH=$(pwd)/BlueWallet_${{ needs.build.outputs.project_version }}_${{ needs.build.outputs.new_build_number }}.ipa" >> $GITHUB_ENV
|
||||
|
@ -205,19 +282,23 @@ jobs:
|
|||
- name: Verify IPA Path Before Upload
|
||||
run: |
|
||||
if [ ! -f "$IPA_OUTPUT_PATH" ]; then
|
||||
echo "IPA file not found at path: $IPA_OUTPUT_PATH"
|
||||
echo "❌ IPA file not found at path: $IPA_OUTPUT_PATH"
|
||||
ls -la $(pwd)
|
||||
exit 1
|
||||
else
|
||||
echo "✅ Found IPA at: $IPA_OUTPUT_PATH"
|
||||
fi
|
||||
|
||||
- name: Print Environment Variables for Debugging
|
||||
run: |
|
||||
echo "LATEST_COMMIT_MESSAGE: $LATEST_COMMIT_MESSAGE"
|
||||
echo "BRANCH_NAME: $BRANCH_NAME"
|
||||
echo "PROJECT_VERSION: $PROJECT_VERSION"
|
||||
echo "NEW_BUILD_NUMBER: $NEW_BUILD_NUMBER"
|
||||
echo "IPA_OUTPUT_PATH: $IPA_OUTPUT_PATH"
|
||||
|
||||
- name: Upload to TestFlight
|
||||
run: |
|
||||
ls -la $IPA_OUTPUT_PATH
|
||||
bundle exec fastlane ios upload_to_testflight_lane
|
||||
run: bundle exec fastlane ios upload_to_testflight_lane
|
||||
env:
|
||||
APP_STORE_CONNECT_API_KEY_PATH: $(pwd)/appstore_api_key.p8
|
||||
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
||||
|
@ -228,18 +309,19 @@ jobs:
|
|||
APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }}
|
||||
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
IPA_OUTPUT_PATH: ${{ env.IPA_OUTPUT_PATH }}
|
||||
|
||||
- name: Post PR Comment
|
||||
if: success() && github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }}
|
||||
PROJECT_VERSION: ${{ needs.build.outputs.project_version }}
|
||||
LATEST_COMMIT_MESSAGE: ${{ needs.build.outputs.latest_commit_message }}
|
||||
with:
|
||||
script: |
|
||||
const buildNumber = process.env.BUILD_NUMBER;
|
||||
const message = `The build ${buildNumber} has been uploaded to TestFlight.`;
|
||||
const version = process.env.PROJECT_VERSION;
|
||||
const message = `✅ Build ${version} (${buildNumber}) has been uploaded to TestFlight and will be available for testing soon.`;
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const repo = context.repo;
|
||||
github.rest.issues.createComment({
|
||||
|
|
4
.github/workflows/build-release-apk.yml
vendored
4
.github/workflows/build-release-apk.yml
vendored
|
@ -4,7 +4,7 @@ on:
|
|||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
types: [opened, synchronize, reopened]
|
||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
@ -102,7 +102,7 @@ jobs:
|
|||
browserstack:
|
||||
runs-on: ubuntu-latest
|
||||
needs: buildReleaseApk
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
if: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'browserstack') }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
17
App.tsx
17
App.tsx
|
@ -1,5 +1,3 @@
|
|||
import 'react-native-gesture-handler'; // should be on top
|
||||
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import React from 'react';
|
||||
import { useColorScheme } from 'react-native';
|
||||
|
@ -9,23 +7,26 @@ import { SettingsProvider } from './components/Context/SettingsProvider';
|
|||
import { BlueDarkTheme, BlueDefaultTheme } from './components/themes';
|
||||
import MasterView from './navigation/MasterView';
|
||||
import { navigationRef } from './NavigationService';
|
||||
import { useLogger } from '@react-navigation/devtools';
|
||||
import { StorageProvider } from './components/Context/StorageProvider';
|
||||
|
||||
const App = () => {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
useLogger(navigationRef);
|
||||
|
||||
return (
|
||||
<LargeScreenProvider>
|
||||
<NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}>
|
||||
<SafeAreaProvider>
|
||||
<NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}>
|
||||
<SafeAreaProvider>
|
||||
<LargeScreenProvider>
|
||||
<StorageProvider>
|
||||
<SettingsProvider>
|
||||
<MasterView />
|
||||
</SettingsProvider>
|
||||
</StorageProvider>
|
||||
</SafeAreaProvider>
|
||||
</NavigationContainer>
|
||||
</LargeScreenProvider>
|
||||
</LargeScreenProvider>
|
||||
</SafeAreaProvider>
|
||||
</NavigationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -40,9 +40,16 @@ export const BlueCard = props => {
|
|||
return <View {...props} style={{ padding: 20 }} />;
|
||||
};
|
||||
|
||||
export const BlueText = props => {
|
||||
export const BlueText = ({ bold = false, ...props }) => {
|
||||
const { colors } = useTheme();
|
||||
const style = StyleSheet.compose({ color: colors.foregroundColor, writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr' }, props.style);
|
||||
const style = StyleSheet.compose(
|
||||
{
|
||||
color: colors.foregroundColor,
|
||||
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
|
||||
fontWeight: bold ? 'bold' : 'normal',
|
||||
},
|
||||
props.style,
|
||||
);
|
||||
return <Text {...props} style={style} />;
|
||||
};
|
||||
|
||||
|
@ -75,6 +82,7 @@ export const BlueFormMultiInput = props => {
|
|||
multiline
|
||||
underlineColorAndroid="transparent"
|
||||
numberOfLines={4}
|
||||
editable={!props.editable}
|
||||
style={{
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 16,
|
||||
|
|
10
Gemfile
10
Gemfile
|
@ -3,8 +3,14 @@ source "https://rubygems.org"
|
|||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||
ruby "3.1.6"
|
||||
gem 'rubyzip', '2.4.1'
|
||||
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
|
||||
gem 'cocoapods', '~> 1.14.3'
|
||||
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
|
||||
gem "fastlane", ">= 2.225.0"
|
||||
gem "fastlane", "~> 2.226.0"
|
||||
gem 'xcodeproj', '< 1.26.0'
|
||||
gem 'concurrent-ruby', '< 1.3.4'
|
||||
|
||||
# Required for App Store Connect API
|
||||
gem "jwt"
|
||||
|
||||
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
||||
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||
|
|
107
Gemfile.lock
107
Gemfile.lock
|
@ -5,7 +5,7 @@ GEM
|
|||
base64
|
||||
nkf
|
||||
rexml
|
||||
activesupport (7.2.2)
|
||||
activesupport (7.2.2.1)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
|
@ -24,31 +24,32 @@ GEM
|
|||
json (>= 1.5.1)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1002.0)
|
||||
aws-sdk-core (3.212.0)
|
||||
aws-eventstream (1.3.1)
|
||||
aws-partitions (1.1058.0)
|
||||
aws-sdk-core (3.219.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.95.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (1.99.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.170.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-s3 (1.182.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-sigv4 (1.11.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.8)
|
||||
bigdecimal (3.1.9)
|
||||
claide (1.1.0)
|
||||
cocoapods (1.16.2)
|
||||
cocoapods (1.14.3)
|
||||
addressable (~> 2.8)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.16.2)
|
||||
cocoapods-core (= 1.14.3)
|
||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||
cocoapods-downloader (>= 2.1, < 3.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
|
@ -62,8 +63,8 @@ GEM
|
|||
molinillo (~> 0.8.0)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (>= 2.3.0, < 3.0)
|
||||
xcodeproj (>= 1.27.0, < 2.0)
|
||||
cocoapods-core (1.16.2)
|
||||
xcodeproj (>= 1.23.0, < 2.0)
|
||||
cocoapods-core (1.14.3)
|
||||
activesupport (>= 5.0, < 8)
|
||||
addressable (~> 2.8)
|
||||
algoliasearch (~> 1.0)
|
||||
|
@ -86,10 +87,10 @@ GEM
|
|||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
concurrent-ruby (1.3.4)
|
||||
connection_pool (2.4.1)
|
||||
concurrent-ruby (1.3.3)
|
||||
connection_pool (2.5.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.5)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
|
@ -118,8 +119,8 @@ GEM
|
|||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-multipart (1.1.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
|
@ -127,8 +128,8 @@ GEM
|
|||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.225.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.226.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
|
@ -168,17 +169,25 @@ GEM
|
|||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty (~> 0.4.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-plugin-browserstack (0.3.3)
|
||||
rest-client (~> 2.0, >= 2.0.2)
|
||||
fastlane-plugin-bugsnag (2.3.1)
|
||||
git
|
||||
xml-simple
|
||||
fastlane-plugin-bugsnag_sourcemaps_upload (0.2.0)
|
||||
fastlane-sirp (1.0.0)
|
||||
sysrandom (~> 1.0)
|
||||
ffi (1.17.0)
|
||||
ffi (1.17.1)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
git (3.0.0)
|
||||
activesupport (>= 5.0)
|
||||
addressable (~> 2.8)
|
||||
process_executer (~> 1.3)
|
||||
rchardet (~> 1.9)
|
||||
google-apis-androidpublisher_v3 (0.54.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.3)
|
||||
|
@ -217,36 +226,40 @@ GEM
|
|||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.7)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.6)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.8.1)
|
||||
jwt (2.9.3)
|
||||
json (2.10.1)
|
||||
jwt (2.10.1)
|
||||
base64
|
||||
logger (1.6.1)
|
||||
logger (1.6.6)
|
||||
mime-types (3.6.0)
|
||||
logger
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2024.1105)
|
||||
mime-types-data (3.2025.0220)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.1)
|
||||
minitest (5.25.4)
|
||||
molinillo (0.8.0)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
nanaimo (0.4.0)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
naturally (2.2.1)
|
||||
netrc (0.11.0)
|
||||
nkf (0.2.0)
|
||||
optparse (0.5.0)
|
||||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.1)
|
||||
plist (3.7.2)
|
||||
process_executer (1.3.0)
|
||||
public_suffix (4.0.7)
|
||||
rake (13.2.1)
|
||||
rchardet (1.9.0)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
|
@ -257,12 +270,12 @@ GEM
|
|||
mime-types (>= 1.16, < 4.0)
|
||||
netrc (~> 0.8)
|
||||
retriable (3.1.2)
|
||||
rexml (3.3.9)
|
||||
rouge (2.0.7)
|
||||
rexml (3.4.1)
|
||||
rouge (3.28.0)
|
||||
ruby-macho (2.5.1)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
securerandom (0.3.1)
|
||||
securerandom (0.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.19.0)
|
||||
addressable (~> 2.8)
|
||||
|
@ -288,31 +301,37 @@ GEM
|
|||
uber (0.1.0)
|
||||
unicode-display_width (2.6.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.27.0)
|
||||
xcodeproj (1.25.1)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.4.0)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (>= 3.3.6, < 4.0)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty (0.4.0)
|
||||
rouge (~> 3.28.0)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
xml-simple (1.1.9)
|
||||
rexml
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
activesupport (>= 6.1.7.5, != 7.1.0)
|
||||
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
|
||||
fastlane (>= 2.225.0)
|
||||
cocoapods (~> 1.14.3)
|
||||
concurrent-ruby (< 1.3.4)
|
||||
fastlane (~> 2.226.0)
|
||||
fastlane-plugin-browserstack
|
||||
fastlane-plugin-bugsnag
|
||||
fastlane-plugin-bugsnag_sourcemaps_upload
|
||||
jwt
|
||||
rubyzip (= 2.4.1)
|
||||
xcodeproj (< 1.26.0)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.1.6p260
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.18
|
||||
2.3.27
|
||||
|
|
|
@ -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;
|
|
@ -14,10 +14,6 @@ export function dispatch(action: NavigationAction) {
|
|||
}
|
||||
}
|
||||
|
||||
export function navigateToWalletsList() {
|
||||
navigate('WalletsList');
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.current?.reset({
|
||||
|
|
|
@ -73,6 +73,10 @@ def enableProguardInReleaseBuilds = false
|
|||
def jscFlavor = 'org.webkit:android-jsc-intl:+'
|
||||
|
||||
android {
|
||||
androidResources {
|
||||
noCompress += ["bundle"]
|
||||
}
|
||||
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
@ -83,7 +87,7 @@ android {
|
|||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "7.0.8"
|
||||
versionName "7.1.5"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
@ -133,7 +137,7 @@ dependencies {
|
|||
androidTestImplementation('com.wix:detox:0.1.1')
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
}
|
||||
apply plugin: 'com.google.gms.google-services' // Google Services plugin
|
||||
apply plugin: "com.bugsnag.android.gradle"
|
||||
apply plugin: "com.bugsnag.android.gradle"
|
||||
|
|
|
@ -16,13 +16,6 @@
|
|||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.ACTION_OPEN_DOCUMENT" />
|
||||
<uses-permission android:name="android.permission.ACTION_GET_CONTENT" />
|
||||
<uses-permission android:name="android.permission.ACTION_CREATE_DOCUMENT" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.ACTION_SCREEN_OFF"/>
|
||||
<uses-permission android:name="android.permission.ACTION_SCREEN_ON"/>
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
|
@ -58,7 +51,15 @@
|
|||
<meta-data
|
||||
android:name="firebase_analytics_collection_enabled"
|
||||
android:value="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
|
||||
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
|
||||
<receiver
|
||||
|
|
|
@ -293,6 +293,13 @@
|
|||
"symbol": "zł",
|
||||
"country": "Poland (Polish Zloty)"
|
||||
},
|
||||
"PYG": {
|
||||
"endPointKey": "PYG",
|
||||
"locale": "es-PY",
|
||||
"source": "CoinDesk",
|
||||
"symbol": "₲",
|
||||
"country": "Paraguay (Paraguayan Guarani)"
|
||||
},
|
||||
"QAR": {
|
||||
"endPointKey": "QAR",
|
||||
"locale": "ar-QA",
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.appwidget.AppWidgetManager
|
|||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.work.WorkManager
|
||||
|
||||
class BitcoinPriceWidget : AppWidgetProvider() {
|
||||
|
@ -11,7 +12,6 @@ class BitcoinPriceWidget : AppWidgetProvider() {
|
|||
companion object {
|
||||
private const val TAG = "BitcoinPriceWidget"
|
||||
private const val SHARED_PREF_NAME = "group.io.bluewallet.bluewallet"
|
||||
private const val WIDGET_COUNT_KEY = "widget_count"
|
||||
}
|
||||
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
|
@ -24,22 +24,12 @@ class BitcoinPriceWidget : AppWidgetProvider() {
|
|||
|
||||
override fun onEnabled(context: Context) {
|
||||
super.onEnabled(context)
|
||||
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 0)
|
||||
if (widgetCount >= 1) {
|
||||
Log.e(TAG, "Only one widget instance is allowed.")
|
||||
return
|
||||
}
|
||||
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount + 1).apply()
|
||||
Log.d(TAG, "onEnabled called")
|
||||
WidgetUpdateWorker.scheduleWork(context)
|
||||
}
|
||||
|
||||
override fun onDisabled(context: Context) {
|
||||
super.onDisabled(context)
|
||||
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 1)
|
||||
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount - 1).apply()
|
||||
Log.d(TAG, "onDisabled called")
|
||||
clearCache(context)
|
||||
WorkManager.getInstance(context).cancelUniqueWork(WidgetUpdateWorker.WORK_NAME)
|
||||
|
@ -47,9 +37,6 @@ class BitcoinPriceWidget : AppWidgetProvider() {
|
|||
|
||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||
super.onDeleted(context, appWidgetIds)
|
||||
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 1)
|
||||
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount - appWidgetIds.size).apply()
|
||||
Log.d(TAG, "onDeleted called for widgets: ${appWidgetIds.joinToString()}")
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import com.facebook.react.ReactPackage
|
|||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.soloader.SoLoader
|
||||
import com.facebook.react.modules.i18nmanager.I18nUtil
|
||||
|
||||
|
@ -50,8 +51,7 @@ class MainApplication : Application(), ReactApplication {
|
|||
sharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
val sharedI18nUtilInstance = I18nUtil.getInstance()
|
||||
sharedI18nUtilInstance.allowRTL(applicationContext, true)
|
||||
SoLoader.init(this, /* native exopackage */ false)
|
||||
|
||||
SoLoader.init(this, OpenSourceMergedSoMapping)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
|
|
|
@ -56,7 +56,8 @@ object MarketAPI {
|
|||
"CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.lowercase()}"
|
||||
"BNR" -> "https://www.bnr.ro/nbrfxrates.xml"
|
||||
"Kraken" -> "https://api.kraken.com/0/public/Ticker?pair=XXBTZ${endPointKey.uppercase()}"
|
||||
else -> "https://api.coindesk.com/v1/bpi/currentprice/$endPointKey.json"
|
||||
"CoinDesk" -> "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=${endPointKey.uppercase()}"
|
||||
else -> "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=${endPointKey.uppercase()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +74,10 @@ object MarketAPI {
|
|||
"coinpaprika" -> json.getJSONObject("quotes").getJSONObject("INR").getString("price")
|
||||
"Coinbase" -> json.getJSONObject("data").getString("amount")
|
||||
"Kraken" -> json.getJSONObject("result").getJSONObject("XXBTZ${endPointKey.uppercase()}").getJSONArray("c").getString(0)
|
||||
"CoinDesk" -> {
|
||||
val rate = json.optDouble(endPointKey.uppercase(), -1.0)
|
||||
if (rate < 0) null else rate.toString()
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
<TextView
|
||||
android:id="@+id/price_value"
|
||||
style="@style/WidgetTextPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="8dp"
|
||||
|
@ -67,6 +67,7 @@
|
|||
android:autoSizeTextType="uniform"
|
||||
android:duplicateParentState="false"
|
||||
android:editable="false"
|
||||
android:gravity="end"
|
||||
android:lines="1"
|
||||
android:text="Loading..."
|
||||
android:textSize="24sp"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/widget_layout"
|
||||
android:minWidth="160dp"
|
||||
android:minWidth="170dp"
|
||||
android:minHeight="100dp"
|
||||
android:updatePeriodMillis="0"
|
||||
android:widgetCategory="home_screen"
|
||||
|
|
4
android/app/src/main/res/xml/file_paths.xml
Normal file
4
android/app/src/main/res/xml/file_paths.xml
Normal 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>
|
|
@ -3,8 +3,8 @@
|
|||
buildscript {
|
||||
ext {
|
||||
minSdkVersion = 24
|
||||
buildToolsVersion = "34.0.0"
|
||||
compileSdkVersion = 34
|
||||
buildToolsVersion = "35.0.0"
|
||||
compileSdkVersion = 35
|
||||
targetSdkVersion = 34
|
||||
googlePlayServicesVersion = "16.+"
|
||||
googlePlayServicesIidVersion = "16.0.1"
|
||||
|
@ -58,8 +58,8 @@ subprojects {
|
|||
afterEvaluate {project ->
|
||||
if (project.hasProperty("android")) {
|
||||
android {
|
||||
buildToolsVersion "34.0.0"
|
||||
compileSdkVersion 34
|
||||
buildToolsVersion "35.0.0"
|
||||
compileSdkVersion 35
|
||||
defaultConfig {
|
||||
minSdkVersion 24
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
5
android/gradlew
vendored
5
android/gradlew
vendored
|
@ -86,7 +86,8 @@ done
|
|||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
@ -248,4 +249,4 @@ eval "set -- $(
|
|||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
exec "$JAVACMD" "$@"
|
188
android/gradlew.bat
vendored
188
android/gradlew.bat
vendored
|
@ -1,94 +1,94 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
|
|
@ -10,6 +10,7 @@ import loc from '../loc';
|
|||
import { GROUP_IO_BLUEWALLET } from './currency';
|
||||
import { ElectrumServerItem } from '../screen/settings/ElectrumSettings';
|
||||
import { triggerWarningHapticFeedback } from './hapticFeedback';
|
||||
import { AlertButton } from 'react-native';
|
||||
|
||||
const ElectrumClient = require('electrum-client');
|
||||
const net = require('net');
|
||||
|
@ -137,31 +138,40 @@ async function _getRealm() {
|
|||
}
|
||||
|
||||
export const getPreferredServer = async (): Promise<ElectrumServerItem | undefined> => {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
|
||||
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
|
||||
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
|
||||
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
|
||||
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
|
||||
|
||||
console.log('Getting preferred server:', { host, tcpPort, sslPort });
|
||||
console.log('Getting preferred server:', { host, tcpPort, sslPort });
|
||||
|
||||
if (!host) {
|
||||
console.warn('Preferred server host is undefined');
|
||||
return;
|
||||
if (!host) {
|
||||
console.warn('Preferred server host is undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
tcp: tcpPort ? Number(tcpPort) : undefined,
|
||||
ssl: sslPort ? Number(sslPort) : undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in getPreferredServer:', error);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
tcp: tcpPort ? Number(tcpPort) : undefined,
|
||||
ssl: sslPort ? Number(sslPort) : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const removePreferredServer = async () => {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
console.log('Removing preferred server');
|
||||
await DefaultPreference.clear(ELECTRUM_HOST);
|
||||
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
|
||||
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
console.log('Removing preferred server');
|
||||
await DefaultPreference.clear(ELECTRUM_HOST);
|
||||
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
|
||||
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
|
||||
} catch (error) {
|
||||
console.error('Error in removePreferredServer:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export async function isDisabled(): Promise<boolean> {
|
||||
|
@ -203,26 +213,31 @@ function getNextPeer() {
|
|||
}
|
||||
|
||||
async function getSavedPeer(): Promise<Peer | null> {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
|
||||
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
|
||||
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
|
||||
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
|
||||
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
|
||||
|
||||
console.log('Getting saved peer:', { host, tcpPort, sslPort });
|
||||
console.log('Getting saved peer:', { host, tcpPort, sslPort });
|
||||
|
||||
if (!host) {
|
||||
if (!host) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sslPort) {
|
||||
return { host, ssl: Number(sslPort) };
|
||||
}
|
||||
|
||||
if (tcpPort) {
|
||||
return { host, tcp: Number(tcpPort) };
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error in getSavedPeer:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sslPort) {
|
||||
return { host, ssl: Number(sslPort) };
|
||||
}
|
||||
|
||||
if (tcpPort) {
|
||||
return { host, tcp: Number(tcpPort) };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function connectMain(): Promise<void> {
|
||||
|
@ -238,19 +253,6 @@ export async function connectMain(): Promise<void> {
|
|||
|
||||
console.log('Using peer:', JSON.stringify(usingPeer));
|
||||
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
try {
|
||||
if (usingPeer.host.endsWith('onion')) {
|
||||
const randomPeer = getCurrentPeer();
|
||||
await DefaultPreference.set(ELECTRUM_HOST, randomPeer.host);
|
||||
await DefaultPreference.set(ELECTRUM_TCP_PORT, randomPeer.tcp ?? '');
|
||||
await DefaultPreference.set(ELECTRUM_SSL_PORT, randomPeer.ssl ?? '');
|
||||
}
|
||||
} catch (e) {
|
||||
// Must be running on Android
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('begin connection:', JSON.stringify(usingPeer));
|
||||
mainClient = new ElectrumClient(net, tls, usingPeer.ssl || usingPeer.tcp, usingPeer.host, usingPeer.ssl ? 'tls' : 'tcp');
|
||||
|
@ -261,7 +263,8 @@ export async function connectMain(): Promise<void> {
|
|||
// most likely got a timeout from electrum ping. lets reconnect
|
||||
// but only if we were previously connected (mainConnected), otherwise theres other
|
||||
// code which does connection retries
|
||||
mainClient.close();
|
||||
mainClient?.close();
|
||||
mainClient = undefined;
|
||||
mainConnected = false;
|
||||
// dropping `mainConnected` flag ensures there wont be reconnection race condition if several
|
||||
// errors triggered
|
||||
|
@ -309,12 +312,15 @@ export async function connectMain(): Promise<void> {
|
|||
} catch (e) {
|
||||
mainConnected = false;
|
||||
console.log('bad connection:', JSON.stringify(usingPeer), e);
|
||||
mainClient?.close();
|
||||
mainClient = undefined;
|
||||
}
|
||||
|
||||
if (!mainConnected) {
|
||||
console.log('retry');
|
||||
connectionAttempt = connectionAttempt + 1;
|
||||
mainClient.close && mainClient.close();
|
||||
mainClient?.close();
|
||||
mainClient = undefined;
|
||||
if (connectionAttempt >= 5) {
|
||||
presentNetworkErrorAlert(usingPeer);
|
||||
} else {
|
||||
|
@ -326,33 +332,61 @@ export async function connectMain(): Promise<void> {
|
|||
}
|
||||
|
||||
export async function presentResetToDefaultsAlert(): Promise<boolean> {
|
||||
const hasPreferredServer = await getPreferredServer();
|
||||
const serverHistoryStr = await DefaultPreference.get(ELECTRUM_SERVER_HISTORY);
|
||||
const serverHistory = typeof serverHistoryStr === 'string' ? JSON.parse(serverHistoryStr) : [];
|
||||
return new Promise(resolve => {
|
||||
triggerWarningHapticFeedback();
|
||||
|
||||
const buttons: AlertButton[] = [];
|
||||
|
||||
if (hasPreferredServer?.host && (hasPreferredServer.tcp || hasPreferredServer.ssl)) {
|
||||
buttons.push({
|
||||
text: loc.settings.electrum_reset,
|
||||
onPress: async () => {
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
await DefaultPreference.clear(ELECTRUM_HOST);
|
||||
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
|
||||
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
|
||||
} catch (e) {
|
||||
console.log(e); // Must be running on Android
|
||||
}
|
||||
resolve(true);
|
||||
},
|
||||
style: 'default',
|
||||
});
|
||||
}
|
||||
|
||||
if (serverHistory.length > 0) {
|
||||
buttons.push({
|
||||
text: loc.settings.electrum_reset_to_default_and_clear_history,
|
||||
onPress: async () => {
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
await DefaultPreference.clear(ELECTRUM_SERVER_HISTORY);
|
||||
await DefaultPreference.clear(ELECTRUM_HOST);
|
||||
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
|
||||
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
|
||||
} catch (e) {
|
||||
console.log(e); // Must be running on Android
|
||||
}
|
||||
resolve(true);
|
||||
},
|
||||
style: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
text: loc._.cancel,
|
||||
onPress: () => resolve(false),
|
||||
style: 'cancel',
|
||||
});
|
||||
|
||||
presentAlert({
|
||||
title: loc.settings.electrum_reset,
|
||||
message: loc.settings.electrum_reset_to_default,
|
||||
buttons: [
|
||||
{
|
||||
text: loc._.cancel,
|
||||
style: 'cancel',
|
||||
onPress: () => resolve(false),
|
||||
},
|
||||
{
|
||||
text: loc._.ok,
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
await DefaultPreference.clear(ELECTRUM_HOST);
|
||||
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
|
||||
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
|
||||
} catch (e) {
|
||||
console.log(e); // Must be running on Android
|
||||
}
|
||||
resolve(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
buttons,
|
||||
options: { cancelable: true },
|
||||
});
|
||||
});
|
||||
|
@ -378,7 +412,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
|
|||
text: loc.wallets.list_tryagain,
|
||||
onPress: () => {
|
||||
connectionAttempt = 0;
|
||||
mainClient.close() && mainClient.close();
|
||||
mainClient?.close();
|
||||
mainClient = undefined;
|
||||
setTimeout(connectMain, 500);
|
||||
},
|
||||
style: 'default',
|
||||
|
@ -389,7 +424,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
|
|||
presentResetToDefaultsAlert().then(result => {
|
||||
if (result) {
|
||||
connectionAttempt = 0;
|
||||
mainClient.close() && mainClient.close();
|
||||
mainClient?.close();
|
||||
mainClient = undefined;
|
||||
setTimeout(connectMain, 500);
|
||||
}
|
||||
});
|
||||
|
@ -400,7 +436,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
|
|||
text: loc._.cancel,
|
||||
onPress: () => {
|
||||
connectionAttempt = 0;
|
||||
mainClient.close() && mainClient.close();
|
||||
mainClient?.close();
|
||||
mainClient = undefined;
|
||||
},
|
||||
style: 'cancel',
|
||||
},
|
||||
|
@ -445,13 +482,18 @@ async function getRandomDynamicPeer(): Promise<Peer> {
|
|||
}
|
||||
|
||||
export const getBalanceByAddress = async function (address: string): Promise<{ confirmed: number; unconfirmed: number }> {
|
||||
if (!mainClient) throw new Error('Electrum client is not connected');
|
||||
const script = bitcoin.address.toOutputScript(address);
|
||||
const hash = bitcoin.crypto.sha256(script);
|
||||
const reversedHash = Buffer.from(hash).reverse();
|
||||
const balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
|
||||
balance.addr = address;
|
||||
return balance;
|
||||
try {
|
||||
if (!mainClient) throw new Error('Electrum client is not connected');
|
||||
const script = bitcoin.address.toOutputScript(address);
|
||||
const hash = bitcoin.crypto.sha256(script);
|
||||
const reversedHash = Buffer.from(hash).reverse();
|
||||
const balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
|
||||
balance.addr = address;
|
||||
return balance;
|
||||
} catch (error) {
|
||||
console.error('Error in getBalanceByAddress:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getConfig = async function () {
|
||||
|
@ -929,25 +971,29 @@ export async function multiGetTransactionByTxid<T extends boolean>(
|
|||
}
|
||||
|
||||
// saving cache:
|
||||
realm.write(() => {
|
||||
for (const txid of Object.keys(ret)) {
|
||||
const tx = ret[txid];
|
||||
// dont cache immature txs, but only for 'verbose', since its fully decoded tx jsons. non-verbose are just plain
|
||||
// strings txhex
|
||||
if (verbose && typeof tx !== 'string' && (!tx?.confirmations || tx.confirmations < 7)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
realm.write(() => {
|
||||
for (const txid of Object.keys(ret)) {
|
||||
const tx = ret[txid];
|
||||
// dont cache immature txs, but only for 'verbose', since its fully decoded tx jsons. non-verbose are just plain
|
||||
// strings txhex
|
||||
if (verbose && typeof tx !== 'string' && (!tx?.confirmations || tx.confirmations < 7)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
realm.create(
|
||||
'Cache',
|
||||
{
|
||||
cache_key: txid + cacheKeySuffix,
|
||||
cache_value: JSON.stringify(ret[txid]),
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
);
|
||||
}
|
||||
});
|
||||
realm.create(
|
||||
'Cache',
|
||||
{
|
||||
cache_key: txid + cacheKeySuffix,
|
||||
cache_value: JSON.stringify(ret[txid]),
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (writeError) {
|
||||
console.error('Failed to write transaction cache:', writeError);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { Alert, Linking, Platform } from 'react-native';
|
||||
import { Platform } from 'react-native';
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
import RNFS from 'react-native-fs';
|
||||
import { launchImageLibrary, ImagePickerResponse } from 'react-native-image-picker';
|
||||
import Share from 'react-native-share';
|
||||
import { request, PERMISSIONS } from 'react-native-permissions';
|
||||
import presentAlert from '../components/Alert';
|
||||
import loc from '../loc';
|
||||
import { isDesktop } from './environment';
|
||||
|
@ -37,7 +36,7 @@ const _shareOpen = async (filePath: string, showShareDialog: boolean = false) =>
|
|||
|
||||
/**
|
||||
* Writes a file to fs, and triggers an OS sharing dialog, so user can decide where to put this file (share to cloud
|
||||
* or perhabs messaging app). Provided filename should be just a file name, NOT a path
|
||||
* or perhaps messaging app). Provided filename should be just a file name, NOT a path
|
||||
*/
|
||||
|
||||
export const writeFileAndExport = async function (fileName: string, contents: string, showShareDialog: boolean = true) {
|
||||
|
@ -48,36 +47,21 @@ export const writeFileAndExport = async function (fileName: string, contents: st
|
|||
await RNFS.writeFile(filePath, contents);
|
||||
await _shareOpen(filePath, showShareDialog);
|
||||
} else if (Platform.OS === 'android') {
|
||||
const isAndroidVersion33OrAbove = Platform.Version >= 33;
|
||||
const permissionType = isAndroidVersion33OrAbove ? PERMISSIONS.ANDROID.READ_MEDIA_IMAGES : PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE;
|
||||
|
||||
const result = await request(permissionType);
|
||||
if (result === 'granted') {
|
||||
const filePath = `${RNFS.ExternalDirectoryPath}/${sanitizedFileName}`;
|
||||
try {
|
||||
await RNFS.writeFile(filePath, contents);
|
||||
if (showShareDialog) {
|
||||
await _shareOpen(filePath);
|
||||
} else {
|
||||
presentAlert({ message: loc.formatString(loc.send.file_saved_at_path, { filePath }) });
|
||||
}
|
||||
} catch (e: any) {
|
||||
presentAlert({ message: e.message });
|
||||
const filePath = `${RNFS.DownloadDirectoryPath}/${sanitizedFileName}`;
|
||||
try {
|
||||
await RNFS.writeFile(filePath, contents);
|
||||
if (showShareDialog) {
|
||||
await _shareOpen(filePath);
|
||||
} else {
|
||||
presentAlert({ message: loc.formatString(loc.send.file_saved_at_path, { filePath }) });
|
||||
}
|
||||
} else {
|
||||
Alert.alert(loc.send.permission_storage_title, loc.send.permission_storage_denied_message, [
|
||||
{
|
||||
text: loc.send.open_settings,
|
||||
onPress: () => {
|
||||
Linking.openSettings();
|
||||
},
|
||||
style: 'default',
|
||||
},
|
||||
{ text: loc._.cancel, onPress: () => {}, style: 'cancel' },
|
||||
]);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
presentAlert({ message: e.message });
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
presentAlert({ message: error.message });
|
||||
}
|
||||
};
|
||||
|
@ -114,7 +98,7 @@ const _readPsbtFileIntoBase64 = async function (uri: string): Promise<string> {
|
|||
} else {
|
||||
// file was a text file, having base64 psbt in there. so we basically have double base64encoded string
|
||||
// thats why we are returning string that was decoded once;
|
||||
// most likely produced by Coldcard
|
||||
// most likely produced by ColdCard
|
||||
return stringData;
|
||||
}
|
||||
};
|
||||
|
@ -183,23 +167,11 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri
|
|||
if (res.fileCopyUri.toLowerCase().endsWith('.psbt')) {
|
||||
// this is either binary file from ElectrumDesktop OR string file with base64 string in there
|
||||
const file = await _readPsbtFileIntoBase64(fileCopyUri);
|
||||
return { data: file, uri: decodeURI(res.fileCopyUri) };
|
||||
return { data: file, uri: fileCopyUri };
|
||||
}
|
||||
|
||||
if (res.type === DocumentPicker.types.images || res.type?.startsWith('image/')) {
|
||||
try {
|
||||
const uri2 = res.fileCopyUri.replace('file://', '');
|
||||
const result = await RNQRGenerator.detect({ uri: decodeURI(uri2) });
|
||||
if (result) {
|
||||
return { data: result.values[0], uri: fileCopyUri };
|
||||
}
|
||||
presentAlert({ message: loc.send.qr_error_no_qrcode });
|
||||
return { data: false, uri: false };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
presentAlert({ message: loc.send.qr_error_no_qrcode });
|
||||
return { data: false, uri: false };
|
||||
}
|
||||
return await handleImageFile(fileCopyUri);
|
||||
}
|
||||
|
||||
const file = await RNFS.readFile(fileCopyUri);
|
||||
|
@ -212,6 +184,33 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri
|
|||
}
|
||||
};
|
||||
|
||||
const handleImageFile = async (fileCopyUri: string): Promise<{ data: string | false; uri: string | false }> => {
|
||||
try {
|
||||
const exists = await RNFS.exists(fileCopyUri);
|
||||
if (!exists) {
|
||||
presentAlert({ message: 'File does not exist' });
|
||||
return { data: false, uri: false };
|
||||
}
|
||||
// First attempt: use original URI
|
||||
let result = await RNQRGenerator.detect({ uri: decodeURI(fileCopyUri) });
|
||||
if (result?.values && result.values.length > 0) {
|
||||
return { data: result.values[0], uri: fileCopyUri };
|
||||
}
|
||||
// Second attempt: remove file:// prefix and try again
|
||||
const altUri = fileCopyUri.replace(/^file:\/\//, '');
|
||||
result = await RNQRGenerator.detect({ uri: decodeURI(altUri) });
|
||||
if (result?.values && result.values.length > 0) {
|
||||
return { data: result.values[0], uri: fileCopyUri };
|
||||
}
|
||||
presentAlert({ message: loc.send.qr_error_no_qrcode });
|
||||
return { data: false, uri: false };
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
presentAlert({ message: loc.send.qr_error_no_qrcode });
|
||||
return { data: false, uri: false };
|
||||
}
|
||||
};
|
||||
|
||||
export const readFileOutsideSandbox = (filePath: string) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
return readFile(filePath);
|
||||
|
|
|
@ -6,6 +6,7 @@ import { checkNotifications, requestNotifications, RESULTS } from 'react-native-
|
|||
import PushNotification from 'react-native-push-notification';
|
||||
import loc from '../loc';
|
||||
import { groundControlUri } from './constants';
|
||||
import { fetch } from '../util/fetch';
|
||||
|
||||
const PUSH_TOKEN = 'PUSH_TOKEN';
|
||||
const GROUNDCONTROL_BASE_URI = 'GROUNDCONTROL_BASE_URI';
|
||||
|
@ -385,10 +386,6 @@ export const configureNotifications = async onProcessNotifications => {
|
|||
});
|
||||
};
|
||||
|
||||
const _sleep = async ms => {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates whether the provided GroundControl URI is valid by pinging it.
|
||||
*
|
||||
|
@ -396,15 +393,13 @@ const _sleep = async ms => {
|
|||
* @returns {Promise<boolean>} TRUE if valid, FALSE otherwise
|
||||
*/
|
||||
export const isGroundControlUriValid = async uri => {
|
||||
let response;
|
||||
try {
|
||||
response = await Promise.race([fetch(`${uri}/ping`, { headers: _getHeaders() }), _sleep(2000)]);
|
||||
} catch (_) {}
|
||||
|
||||
if (!response) return false;
|
||||
|
||||
const json = await response.json();
|
||||
return !!json.description;
|
||||
const response = await fetch(`${uri}/ping`, { headers: _getHeaders() });
|
||||
const json = await response.json();
|
||||
return !!json.description;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const isNotificationsCapable = hasGmsSync() || hasHmsSync() || Platform.OS !== 'android';
|
||||
|
@ -430,24 +425,21 @@ const getLevels = async () => {
|
|||
const pushToken = await getPushToken();
|
||||
if (!pushToken || !pushToken.token || !pushToken.os) return;
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await Promise.race([
|
||||
fetch(`${baseURI}/getTokenConfiguration`, {
|
||||
method: 'POST',
|
||||
headers: _getHeaders(),
|
||||
body: JSON.stringify({
|
||||
token: pushToken.token,
|
||||
os: pushToken.os,
|
||||
}),
|
||||
const response = await fetch(`${baseURI}/getTokenConfiguration`, {
|
||||
method: 'POST',
|
||||
headers: _getHeaders(),
|
||||
body: JSON.stringify({
|
||||
token: pushToken.token,
|
||||
os: pushToken.os,
|
||||
}),
|
||||
_sleep(3000),
|
||||
]);
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
if (!response) return {};
|
||||
|
||||
return await response.json();
|
||||
if (!response) return {};
|
||||
return await response.json();
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -574,6 +566,9 @@ export const isNotificationsEnabled = async () => {
|
|||
return !isDisabledByUser && !!token && !!levels.level_all;
|
||||
} catch (error) {
|
||||
console.log('Error checking notification levels:', error);
|
||||
if (error instanceof SyntaxError) {
|
||||
throw error;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -157,7 +157,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
||||
|
@ -209,7 +209,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import URL from 'url';
|
||||
import { fetch } from '../util/fetch';
|
||||
|
||||
export default class Azteco {
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import bip21, { TOptions } from 'bip21';
|
||||
import * as bitcoin from 'bitcoinjs-lib';
|
||||
import URL from 'url';
|
||||
|
||||
import { readFileOutsideSandbox } from '../blue_modules/fs';
|
||||
import { Chain } from '../models/bitcoinUnits';
|
||||
import { WatchOnlyWallet } from './';
|
||||
|
@ -87,9 +86,9 @@ class DeeplinkSchemaMatch {
|
|||
} else if (wallet.chain === Chain.OFFCHAIN) {
|
||||
if (action === 'openSend') {
|
||||
completionHandler([
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
walletID: wallet.getID(),
|
||||
},
|
||||
|
@ -157,9 +156,9 @@ class DeeplinkSchemaMatch {
|
|||
]);
|
||||
} else if (DeeplinkSchemaMatch.isLightningInvoice(event.url)) {
|
||||
completionHandler([
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
uri: event.url.replace('://', ':'),
|
||||
},
|
||||
|
@ -182,9 +181,9 @@ class DeeplinkSchemaMatch {
|
|||
// this might be not just an email but a lightning address
|
||||
// @see https://lightningaddress.com
|
||||
completionHandler([
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
uri: event.url,
|
||||
},
|
||||
|
@ -306,9 +305,9 @@ class DeeplinkSchemaMatch {
|
|||
];
|
||||
} else {
|
||||
return [
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
uri: uri.lndInvoice,
|
||||
walletID: wallet.getID(),
|
||||
|
|
|
@ -6,6 +6,7 @@ import CryptoJS from 'crypto-js';
|
|||
// @ts-ignore theres no types for secp256k1
|
||||
import secp256k1 from 'secp256k1';
|
||||
import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api
|
||||
import { fetch } from '../util/fetch';
|
||||
|
||||
const ONION_REGEX = /^(http:\/\/[^/:@]+\.onion(?::\d{1,5})?)(\/.*)?$/; // regex for onion URL
|
||||
|
||||
|
|
|
@ -310,8 +310,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
|||
// then we combine it all together
|
||||
|
||||
const addresses2fetch = [];
|
||||
// Store these values to avoid a race condition if fetchBalance func changes them
|
||||
const next_free_address_index = this.next_free_address_index;
|
||||
const next_free_change_address_index = this.next_free_change_address_index;
|
||||
|
||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||
for (let c = 0; c < next_free_address_index + this.gap_limit; c++) {
|
||||
// external addresses first
|
||||
let hasUnconfirmed = false;
|
||||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
|
||||
|
@ -322,7 +325,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
|||
}
|
||||
}
|
||||
|
||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||
for (let c = 0; c < next_free_change_address_index + this.gap_limit; c++) {
|
||||
// next, internal addresses
|
||||
let hasUnconfirmed = false;
|
||||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
|
||||
|
@ -389,10 +392,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
|||
|
||||
// now purge all unconfirmed txs from internal hashmaps, since some may be evicted from mempool because they became invalid
|
||||
// or replaced. hashmaps are going to be re-populated anyways, since we fetched TXs for addresses with unconfirmed TXs
|
||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||
for (let c = 0; c < next_free_address_index + this.gap_limit; c++) {
|
||||
this._txs_by_external_index[c] = this._txs_by_external_index[c].filter(tx => !!tx.confirmations);
|
||||
}
|
||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||
for (let c = 0; c < next_free_change_address_index + this.gap_limit; c++) {
|
||||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations);
|
||||
}
|
||||
for (const pc of this._receive_payment_codes) {
|
||||
|
@ -404,7 +407,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
|||
// now, we need to put transactions in all relevant `cells` of internal hashmaps:
|
||||
// this._txs_by_internal_index, this._txs_by_external_index & this._txs_by_payment_code_index
|
||||
|
||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||
for (let c = 0; c < next_free_address_index + this.gap_limit; c++) {
|
||||
for (const tx of Object.values(txdatas)) {
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
|
||||
|
@ -445,7 +448,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
|||
}
|
||||
}
|
||||
|
||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||
for (let c = 0; c < next_free_change_address_index + this.gap_limit; c++) {
|
||||
for (const tx of Object.values(txdatas)) {
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import b58 from 'bs58check';
|
||||
import createHash from 'create-hash';
|
||||
import wif from 'wif';
|
||||
|
||||
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||
import { CreateTransactionResult, CreateTransactionUtxo, Transaction, Utxo } from './types';
|
||||
|
@ -211,6 +212,17 @@ export class AbstractWallet {
|
|||
|
||||
setSecret(newSecret: string): this {
|
||||
const origSecret = newSecret;
|
||||
|
||||
// is it minikey https://en.bitcoin.it/wiki/Mini_private_key_format
|
||||
// Starts with S, is 22 length or larger, is base58
|
||||
if (newSecret.startsWith('S') && newSecret.length >= 22 && /^[1-9A-HJ-NP-Za-km-z]+$/.test(newSecret)) {
|
||||
// minikey + ? hashed with SHA256 starts with 0x00 byte
|
||||
if (createHash('sha256').update(`${newSecret}?`).digest('hex').startsWith('00')) {
|
||||
// it is a valid minikey
|
||||
newSecret = wif.encode(0x80, createHash('sha256').update(newSecret).digest(), false);
|
||||
}
|
||||
}
|
||||
|
||||
this.secret = newSecret.trim().replace('bitcoin:', '').replace('BITCOIN:', '');
|
||||
|
||||
if (this.secret.startsWith('BC1')) this.secret = this.secret.toLowerCase();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import bolt11 from 'bolt11';
|
||||
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||
import { LegacyWallet } from './legacy-wallet';
|
||||
import { fetch } from '../../util/fetch';
|
||||
|
||||
export class LightningCustodianWallet extends LegacyWallet {
|
||||
static readonly type = 'lightningCustodianWallet';
|
||||
|
|
|
@ -1090,9 +1090,8 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
|
|||
}
|
||||
|
||||
getID() {
|
||||
const string2hash = [...this._cosignersFingerprints].sort().join(',');
|
||||
const id = createHash('sha256').update(string2hash).digest().toString('hex');
|
||||
return id;
|
||||
const string2hash = [...this._cosigners].sort().join(',') + ';' + [...this._cosignersFingerprints].sort().join(',');
|
||||
return createHash('sha256').update(string2hash).digest().toString('hex');
|
||||
}
|
||||
|
||||
calculateFeeFromPsbt(psbt: Psbt) {
|
||||
|
|
|
@ -79,6 +79,20 @@ export type TransactionOutput = {
|
|||
};
|
||||
};
|
||||
|
||||
export interface DecodedInvoice {
|
||||
destination: string;
|
||||
payment_hash: string;
|
||||
num_satoshis: number;
|
||||
timestamp: number;
|
||||
expiry: number;
|
||||
description: string;
|
||||
description_hash: string;
|
||||
fallback_addr: string;
|
||||
cltv_expiry: string;
|
||||
route_hints: any[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type LightningTransaction = {
|
||||
memo?: string;
|
||||
type?: 'user_invoice' | 'payment_request' | 'bitcoind_tx' | 'paid_invoice';
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useTheme } from './themes';
|
|||
import ToolTipMenu from './TooltipMenu';
|
||||
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
|
||||
import loc from '../loc';
|
||||
import { navigationRef } from '../NavigationService';
|
||||
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
|
||||
|
||||
type AddWalletButtonProps = {
|
||||
onPress?: (event: GestureResponderEvent) => void;
|
||||
|
@ -23,21 +23,25 @@ const styles = StyleSheet.create({
|
|||
|
||||
const AddWalletButton: React.FC<AddWalletButtonProps> = ({ onPress }) => {
|
||||
const { colors } = useTheme();
|
||||
const navigation = useExtendedNavigation();
|
||||
const stylesHook = StyleSheet.create({
|
||||
ball: {
|
||||
backgroundColor: colors.buttonBackgroundColor,
|
||||
},
|
||||
});
|
||||
|
||||
const onPressMenuItem = useCallback((action: string) => {
|
||||
switch (action) {
|
||||
case CommonToolTipActions.ImportWallet.id:
|
||||
navigationRef.current?.navigate('AddWalletRoot', { screen: 'ImportWallet' });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, []);
|
||||
const onPressMenuItem = useCallback(
|
||||
(action: string) => {
|
||||
switch (action) {
|
||||
case CommonToolTipActions.ImportWallet.id:
|
||||
navigation.navigate('AddWalletRoot', { screen: 'ImportWallet' });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[navigation],
|
||||
);
|
||||
|
||||
const actions = useMemo(() => [CommonToolTipActions.ImportWallet], []);
|
||||
|
||||
|
|
|
@ -1,23 +1,18 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { Keyboard, StyleProp, StyleSheet, TextInput, View, ViewStyle } from 'react-native';
|
||||
import React from 'react';
|
||||
import { StyleProp, StyleSheet, TextInput, View, ViewStyle } from 'react-native';
|
||||
import loc from '../loc';
|
||||
import { AddressInputScanButton } from './AddressInputScanButton';
|
||||
import { useTheme } from './themes';
|
||||
import DeeplinkSchemaMatch from '../class/deeplink-schema-match';
|
||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
|
||||
|
||||
interface AddressInputProps {
|
||||
isLoading?: boolean;
|
||||
address?: string;
|
||||
placeholder?: string;
|
||||
onChangeText: (text: string) => void;
|
||||
onBarScanned: (ret: { data?: any }) => void;
|
||||
scanButtonTapped?: () => void;
|
||||
launchedBy?: string;
|
||||
editable?: boolean;
|
||||
inputAccessoryViewID?: string;
|
||||
onBlur?: () => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
testID?: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
keyboardType?:
|
||||
|
@ -42,13 +37,10 @@ const AddressInput = ({
|
|||
testID = 'AddressInput',
|
||||
placeholder = loc.send.details_address,
|
||||
onChangeText,
|
||||
onBarScanned,
|
||||
scanButtonTapped = () => {},
|
||||
launchedBy,
|
||||
editable = true,
|
||||
inputAccessoryViewID,
|
||||
onBlur = () => {},
|
||||
onFocus = () => {},
|
||||
onBlur = () => {},
|
||||
keyboardType = 'default',
|
||||
style,
|
||||
}: AddressInputProps) => {
|
||||
|
@ -64,24 +56,6 @@ const AddressInput = ({
|
|||
},
|
||||
});
|
||||
|
||||
const validateAddressWithFeedback = useCallback((value: string) => {
|
||||
const isBitcoinAddress = DeeplinkSchemaMatch.isBitcoinAddress(value);
|
||||
const isLightningInvoice = DeeplinkSchemaMatch.isLightningInvoice(value);
|
||||
const isValid = isBitcoinAddress || isLightningInvoice;
|
||||
|
||||
triggerHapticFeedback(isValid ? HapticFeedbackTypes.NotificationSuccess : HapticFeedbackTypes.NotificationError);
|
||||
return {
|
||||
isValid,
|
||||
type: isBitcoinAddress ? 'bitcoin' : isLightningInvoice ? 'lightning' : 'invalid',
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onBlurEditing = () => {
|
||||
validateAddressWithFeedback(address);
|
||||
onBlur();
|
||||
Keyboard.dismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.root, stylesHook.root, style]}>
|
||||
<TextInput
|
||||
|
@ -95,21 +69,13 @@ const AddressInput = ({
|
|||
multiline={!editable}
|
||||
inputAccessoryViewID={inputAccessoryViewID}
|
||||
clearButtonMode="while-editing"
|
||||
onBlur={onBlurEditing}
|
||||
onFocus={onFocus}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType={keyboardType}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
{editable ? (
|
||||
<AddressInputScanButton
|
||||
isLoading={isLoading}
|
||||
launchedBy={launchedBy}
|
||||
scanButtonTapped={scanButtonTapped}
|
||||
onBarScanned={onBarScanned}
|
||||
onChangeText={onChangeText}
|
||||
/>
|
||||
) : null}
|
||||
{editable ? <AddressInputScanButton isLoading={isLoading} onChangeText={onChangeText} /> : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Image, Keyboard, Platform, StyleSheet, Text } from 'react-native';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import ToolTipMenu from './TooltipMenu';
|
||||
|
@ -9,33 +9,27 @@ import { useTheme } from './themes';
|
|||
import RNQRGenerator from 'rn-qr-generator';
|
||||
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
|
||||
import { useSettings } from '../hooks/context/useSettings';
|
||||
import { useRoute } from '@react-navigation/native';
|
||||
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
|
||||
|
||||
interface AddressInputScanButtonProps {
|
||||
isLoading: boolean;
|
||||
launchedBy?: string;
|
||||
scanButtonTapped: () => void;
|
||||
onBarScanned: (ret: { data?: any }) => void;
|
||||
isLoading?: boolean;
|
||||
onChangeText: (text: string) => void;
|
||||
}
|
||||
|
||||
interface RouteParams {
|
||||
onBarScanned?: any;
|
||||
type?: 'default' | 'link';
|
||||
testID?: string;
|
||||
beforePress?: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
export const AddressInputScanButton = ({
|
||||
isLoading,
|
||||
launchedBy,
|
||||
scanButtonTapped,
|
||||
onBarScanned,
|
||||
onChangeText,
|
||||
type = 'default',
|
||||
testID = 'BlueAddressInputScanQrButton',
|
||||
beforePress,
|
||||
}: AddressInputScanButtonProps) => {
|
||||
const { colors } = useTheme();
|
||||
const { isClipboardGetContentEnabled } = useSettings();
|
||||
|
||||
const navigation = useExtendedNavigation();
|
||||
const params = useRoute().params as RouteParams;
|
||||
const stylesHook = StyleSheet.create({
|
||||
scan: {
|
||||
backgroundColor: colors.scanLabel,
|
||||
|
@ -46,16 +40,17 @@ export const AddressInputScanButton = ({
|
|||
});
|
||||
|
||||
const toolTipOnPress = useCallback(async () => {
|
||||
await scanButtonTapped();
|
||||
if (beforePress) {
|
||||
await beforePress();
|
||||
}
|
||||
Keyboard.dismiss();
|
||||
navigation.navigate('ScanQRCode', {
|
||||
showFileImportButton: true,
|
||||
});
|
||||
}, [navigation, scanButtonTapped]);
|
||||
}, [navigation, beforePress]);
|
||||
|
||||
const actions = useMemo(() => {
|
||||
const availableActions = [
|
||||
CommonToolTipActions.ScanQR,
|
||||
CommonToolTipActions.ChoosePhoto,
|
||||
CommonToolTipActions.ImportFile,
|
||||
{
|
||||
|
@ -67,20 +62,10 @@ export const AddressInputScanButton = ({
|
|||
return availableActions;
|
||||
}, [isClipboardGetContentEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
const data = params.onBarScanned;
|
||||
if (data) {
|
||||
onBarScanned({ data });
|
||||
navigation.setParams({ onBarScanned: undefined });
|
||||
}
|
||||
});
|
||||
|
||||
const onMenuItemPressed = useCallback(
|
||||
async (action: string) => {
|
||||
if (onBarScanned === undefined) throw new Error('onBarScanned is required');
|
||||
switch (action) {
|
||||
case CommonToolTipActions.ScanQR.id:
|
||||
scanButtonTapped();
|
||||
navigation.navigate('ScanQRCode', {
|
||||
showFileImportButton: true,
|
||||
});
|
||||
|
@ -147,7 +132,7 @@ export const AddressInputScanButton = ({
|
|||
}
|
||||
Keyboard.dismiss();
|
||||
},
|
||||
[navigation, onBarScanned, onChangeText, scanButtonTapped],
|
||||
[navigation, onChangeText],
|
||||
);
|
||||
|
||||
const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]);
|
||||
|
@ -157,21 +142,29 @@ export const AddressInputScanButton = ({
|
|||
actions={actions}
|
||||
isButton
|
||||
onPressMenuItem={onMenuItemPressed}
|
||||
testID="BlueAddressInputScanQrButton"
|
||||
testID={testID}
|
||||
disabled={isLoading}
|
||||
onPress={toolTipOnPress}
|
||||
buttonStyle={buttonStyle}
|
||||
buttonStyle={type === 'default' ? buttonStyle : undefined}
|
||||
accessibilityLabel={loc.send.details_scan}
|
||||
accessibilityHint={loc.send.details_scan_hint}
|
||||
>
|
||||
<Image source={require('../img/scan-white.png')} accessible={false} />
|
||||
<Text style={[styles.scanText, stylesHook.scanText]} accessible={false}>
|
||||
{loc.send.details_scan}
|
||||
</Text>
|
||||
{type === 'default' ? (
|
||||
<>
|
||||
<Image source={require('../img/scan-white.png')} accessible={false} />
|
||||
<Text style={[styles.scanText, stylesHook.scanText]} accessible={false}>
|
||||
{loc.send.details_scan}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text style={[styles.linkText, { color: colors.foregroundColor }]}>{loc.wallets.import_scan_qr}</Text>
|
||||
)}
|
||||
</ToolTipMenu>
|
||||
);
|
||||
};
|
||||
|
||||
AddressInputScanButton.displayName = 'AddressInputScanButton';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scan: {
|
||||
height: 36,
|
||||
|
@ -186,4 +179,8 @@ const styles = StyleSheet.create({
|
|||
scanText: {
|
||||
marginLeft: 4,
|
||||
},
|
||||
linkText: {
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -146,7 +146,9 @@ class AmountInput extends Component {
|
|||
textInput = React.createRef();
|
||||
|
||||
handleTextInputOnPress = () => {
|
||||
this.textInput.current.focus();
|
||||
if (this.textInput && this.textInput.current && typeof this.textInput.current.focus === 'function') {
|
||||
this.textInput.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
handleChangeText = text => {
|
||||
|
@ -258,7 +260,11 @@ class AmountInput extends Component {
|
|||
accessibilityRole="button"
|
||||
accessibilityLabel={loc._.enter_amount}
|
||||
disabled={this.props.pointerEvents === 'none'}
|
||||
onPress={() => this.textInput.focus()}
|
||||
onPress={() => {
|
||||
if (this.textInput && this.textInput.current && typeof this.textInput.current.focus === 'function') {
|
||||
this.textInput.current.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<View style={styles.root}>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { forwardRef, useImperativeHandle, useRef, ReactElement, ComponentType } from 'react';
|
||||
import { SheetSize, SizeInfo, TrueSheet, TrueSheetProps } from '@lodev09/react-native-true-sheet';
|
||||
import { SheetSize, SizeChangeEvent, TrueSheet, TrueSheetProps } from '@lodev09/react-native-true-sheet';
|
||||
import { Keyboard, Image, StyleSheet, View, TouchableOpacity, Platform, GestureResponderEvent, Text } from 'react-native';
|
||||
import SaveFileButton from './SaveFileButton';
|
||||
import { useTheme } from './themes';
|
||||
|
@ -14,7 +14,7 @@ interface BottomModalProps extends TrueSheetProps {
|
|||
footer?: ReactElement | ComponentType<any> | null;
|
||||
footerDefaultMargins?: boolean | number;
|
||||
onPresent?: () => void;
|
||||
onSizeChange?: (size: SizeInfo) => void;
|
||||
onSizeChange?: (event: SizeChangeEvent) => void;
|
||||
showCloseButton?: boolean;
|
||||
shareContent?: BottomModalShareContent;
|
||||
shareButtonOnPress?: (event: GestureResponderEvent) => void;
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import { Animated, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { Animated, Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
// @ts-ignore: no declaration file yet
|
||||
import { Camera, CameraApi, CameraType, Orientation } from 'react-native-camera-kit';
|
||||
import loc from '../loc';
|
||||
import { Icon } from '@rneui/base';
|
||||
import { OnOrientationChangeData, OnReadCodeData } from 'react-native-camera-kit/dist/CameraProps';
|
||||
import { triggerSelectionHapticFeedback } from '../blue_modules/hapticFeedback';
|
||||
import { isDesktop } from '../blue_modules/environment';
|
||||
// @ts-ignore: no declaration file yet
|
||||
import { OnOrientationChangeData, OnReadCodeData } from 'react-native-camera-kit/dist/CameraProps';
|
||||
|
||||
interface CameraScreenProps {
|
||||
onCancelButtonPress: () => void;
|
||||
|
@ -46,8 +49,8 @@ const CameraScreen: React.FC<CameraScreenProps> = ({
|
|||
// For real phone apps, lock your UI orientation using a library like 'react-native-orientation-locker'
|
||||
const rotateUi = true;
|
||||
const uiRotation = orientationAnim.interpolate({
|
||||
inputRange: [1, 4],
|
||||
outputRange: ['180deg', '-90deg'],
|
||||
inputRange: [1, 2, 3, 4],
|
||||
outputRange: ['180deg', '90deg', '0deg', '-90deg'],
|
||||
});
|
||||
const uiRotationStyle = rotateUi ? { transform: [{ rotate: uiRotation }] } : {};
|
||||
|
||||
|
@ -95,68 +98,104 @@ const CameraScreen: React.FC<CameraScreenProps> = ({
|
|||
|
||||
return (
|
||||
<View style={styles.screen}>
|
||||
<View style={styles.topButtons}>
|
||||
<TouchableOpacity style={styles.topButton} onPress={onSetTorch}>
|
||||
<Animated.View style={[styles.topButtonImg, uiRotationStyle]}>
|
||||
<Icon name={torchMode ? 'flashlight-on' : 'flashlight-off'} type="font-awesome-6" color="#ffffff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.rightButtonsContainer}>
|
||||
{showImagePickerButton && (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={loc._.pick_image}
|
||||
style={[styles.topButton, styles.spacing, uiRotationStyle]}
|
||||
onPress={onImagePickerButtonPress}
|
||||
>
|
||||
<Animated.View style={[styles.topButtonImg, uiRotationStyle]}>
|
||||
<Icon name="image" type="font-awesome" color="#ffffff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{showFilePickerButton && (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={loc._.pick_file}
|
||||
style={[styles.topButton, styles.spacing, uiRotationStyle]}
|
||||
onPress={onFilePickerButtonPress}
|
||||
>
|
||||
<Animated.View style={[styles.topButtonImg, uiRotationStyle]}>
|
||||
<Icon name="file-import" type="font-awesome-5" color="#ffffff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{/* Render top buttons only if not desktop as they would not be relevant */}
|
||||
{!isDesktop && (
|
||||
<View style={styles.topButtons}>
|
||||
<TouchableOpacity style={[styles.topButton, uiRotationStyle, torchMode ? styles.activeTorch : {}]} onPress={onSetTorch}>
|
||||
<Animated.View style={styles.topButtonImg}>
|
||||
{Platform.OS === 'ios' ? (
|
||||
<Icon name={torchMode ? 'flashlight-on' : 'flashlight-off'} type="font-awesome-6" color={torchMode ? '#000' : '#fff'} />
|
||||
) : (
|
||||
<Icon name={torchMode ? 'flash-on' : 'flash-off'} type="ionicons" color={torchMode ? '#000' : '#fff'} />
|
||||
)}
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.rightButtonsContainer}>
|
||||
{showImagePickerButton && (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={loc._.pick_image}
|
||||
style={[styles.topButton, styles.spacing, uiRotationStyle]}
|
||||
onPress={onImagePickerButtonPress}
|
||||
>
|
||||
<Animated.View style={styles.topButtonImg}>
|
||||
<Icon name="image" type="font-awesome" color="#ffffff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{showFilePickerButton && (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={loc._.pick_file}
|
||||
style={[styles.topButton, styles.spacing, uiRotationStyle]}
|
||||
onPress={onFilePickerButtonPress}
|
||||
>
|
||||
<Animated.View style={styles.topButtonImg}>
|
||||
<Icon name="file-import" type="font-awesome-5" color="#ffffff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
)}
|
||||
<View style={styles.cameraContainer}>
|
||||
<Camera
|
||||
ref={cameraRef}
|
||||
style={styles.cameraPreview}
|
||||
cameraType={cameraType}
|
||||
resetFocusWhenMotionDetected
|
||||
zoom={zoom}
|
||||
maxZoom={10}
|
||||
scanBarcode
|
||||
resizeMode="cover"
|
||||
onZoom={handleZoom}
|
||||
onReadCode={handleReadCode}
|
||||
torchMode={torchMode ? 'on' : 'off'}
|
||||
shutterPhotoSound
|
||||
maxPhotoQualityPrioritization="quality"
|
||||
resetFocusWhenMotionDetected
|
||||
zoom={zoom}
|
||||
onZoom={handleZoom}
|
||||
maxZoom={10}
|
||||
onOrientationChange={handleOrientationChange}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.bottomButtons}>
|
||||
<TouchableOpacity onPress={onCancelButtonPress}>
|
||||
<Animated.Text style={[styles.backTextStyle, uiRotationStyle]}>{loc._.cancel}</Animated.Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.bottomButton} onPress={onSwitchCameraPressed}>
|
||||
<Animated.View style={[styles.topButtonImg, uiRotationStyle]}>
|
||||
<Icon name="cameraswitch" type="font-awesome-6" color="#ffffff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
{isDesktop ? (
|
||||
<View style={styles.rightButtonsContainer}>
|
||||
{showImagePickerButton && (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={loc._.pick_image}
|
||||
style={[styles.bottomButton, styles.spacing, uiRotationStyle]}
|
||||
onPress={onImagePickerButtonPress}
|
||||
>
|
||||
<Animated.View style={styles.topButtonImg}>
|
||||
<Icon name="image" type="font-awesome" color="#ffffff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{showFilePickerButton && (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={loc._.pick_file}
|
||||
style={[styles.bottomButton, styles.spacing, uiRotationStyle]}
|
||||
onPress={onFilePickerButtonPress}
|
||||
>
|
||||
<Animated.View style={styles.topButtonImg}>
|
||||
<Icon name="file-import" type="font-awesome-5" color="#ffffff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<TouchableOpacity style={[styles.bottomButton, uiRotationStyle]} onPress={onSwitchCameraPressed}>
|
||||
<Animated.View style={[styles.topButtonImg, uiRotationStyle]}>
|
||||
{Platform.OS === 'ios' ? (
|
||||
<Icon name="cameraswitch" type="font-awesome-6" color="#ffffff" />
|
||||
) : (
|
||||
<Icon name={cameraType === CameraType.Back ? 'camera-rear' : 'camera-front'} type="ionicons" color="#ffffff" />
|
||||
)}
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
@ -165,6 +204,9 @@ const CameraScreen: React.FC<CameraScreenProps> = ({
|
|||
export default CameraScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
activeTorch: {
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
screen: {
|
||||
height: '100%',
|
||||
backgroundColor: '#000000',
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import React, { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { InteractionManager } from 'react-native';
|
||||
import { InteractionManager, LayoutAnimation } from 'react-native';
|
||||
import A from '../../blue_modules/analytics';
|
||||
import { BlueApp as BlueAppClass, LegacyWallet, TCounterpartyMetadata, TTXMetadata, WatchOnlyWallet } from '../../class';
|
||||
import type { TWallet } from '../../class/wallets/types';
|
||||
import presentAlert from '../../components/Alert';
|
||||
import loc from '../../loc';
|
||||
import loc, { formatBalanceWithoutSuffix } from '../../loc';
|
||||
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
|
||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
|
||||
import { startAndDecrypt } from '../../blue_modules/start-and-decrypt';
|
||||
import { majorTomToGroundControl } from '../../blue_modules/notifications';
|
||||
import { isNotificationsEnabled, majorTomToGroundControl, unsubscribe } from '../../blue_modules/notifications';
|
||||
import { BitcoinUnit } from '../../models/bitcoinUnits';
|
||||
|
||||
const BlueApp = BlueAppClass.getInstance();
|
||||
|
||||
|
@ -49,6 +50,8 @@ interface StorageContextType {
|
|||
cachedPassword: typeof BlueApp.cachedPassword;
|
||||
getItem: typeof BlueApp.getItem;
|
||||
setItem: typeof BlueApp.setItem;
|
||||
handleWalletDeletion: (walletID: string, forceDelete?: boolean) => Promise<boolean>;
|
||||
confirmWalletDeletion: (wallet: any, onConfirmed: () => void) => void;
|
||||
}
|
||||
|
||||
export enum WalletTransactionsStatus {
|
||||
|
@ -99,6 +102,120 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
|
|||
setWallets([...BlueApp.getWallets()]);
|
||||
}, []);
|
||||
|
||||
const handleWalletDeletion = useCallback(
|
||||
async (walletID: string, forceDelete = false): Promise<boolean> => {
|
||||
console.debug(`handleWalletDeletion: invoked for walletID ${walletID}`);
|
||||
const wallet = wallets.find(w => w.getID() === walletID);
|
||||
if (!wallet) {
|
||||
console.warn(`handleWalletDeletion: wallet not found for ${walletID}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (forceDelete) {
|
||||
deleteWallet(wallet);
|
||||
await saveToDisk(true);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
return true;
|
||||
}
|
||||
|
||||
let isNotificationsSettingsEnabled = false;
|
||||
try {
|
||||
isNotificationsSettingsEnabled = await isNotificationsEnabled();
|
||||
} catch (error) {
|
||||
console.error(`handleWalletDeletion: error checking notifications for wallet ${walletID}`, error);
|
||||
return await new Promise<boolean>(resolve => {
|
||||
presentAlert({
|
||||
title: loc.errors.error,
|
||||
message: loc.wallets.details_delete_wallet_error_message,
|
||||
buttons: [
|
||||
{
|
||||
text: loc.wallets.details_delete_anyway,
|
||||
onPress: async () => {
|
||||
const result = await handleWalletDeletion(walletID, true);
|
||||
resolve(result);
|
||||
},
|
||||
style: 'destructive',
|
||||
},
|
||||
{
|
||||
text: loc.wallets.list_tryagain,
|
||||
onPress: async () => {
|
||||
const result = await handleWalletDeletion(walletID);
|
||||
resolve(result);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: loc._.cancel,
|
||||
onPress: () => resolve(false),
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
options: { cancelable: false },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (isNotificationsSettingsEnabled) {
|
||||
const externalAddresses = wallet.getAllExternalAddresses();
|
||||
if (externalAddresses.length > 0) {
|
||||
console.debug(`handleWalletDeletion: unsubscribing addresses for wallet ${walletID}`);
|
||||
try {
|
||||
await unsubscribe(externalAddresses, [], []);
|
||||
console.debug(`handleWalletDeletion: unsubscribe succeeded for wallet ${walletID}`);
|
||||
} catch (unsubscribeError) {
|
||||
console.error(`handleWalletDeletion: unsubscribe failed for wallet ${walletID}`, unsubscribeError);
|
||||
presentAlert({
|
||||
title: loc.errors.error,
|
||||
message: loc.wallets.details_delete_wallet_error_message,
|
||||
buttons: [{ text: loc._.ok, onPress: () => {} }],
|
||||
options: { cancelable: false },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
deleteWallet(wallet);
|
||||
console.debug(`handleWalletDeletion: wallet ${walletID} deleted successfully`);
|
||||
await saveToDisk(true);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
return true;
|
||||
} catch (e: unknown) {
|
||||
console.error(`handleWalletDeletion: encountered error for wallet ${walletID}`, e);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
return await new Promise<boolean>(resolve => {
|
||||
presentAlert({
|
||||
title: loc.errors.error,
|
||||
message: loc.wallets.details_delete_wallet_error_message,
|
||||
buttons: [
|
||||
{
|
||||
text: loc.wallets.details_delete_anyway,
|
||||
onPress: async () => {
|
||||
const result = await handleWalletDeletion(walletID, true);
|
||||
resolve(result);
|
||||
},
|
||||
style: 'destructive',
|
||||
},
|
||||
{
|
||||
text: loc.wallets.list_tryagain,
|
||||
onPress: async () => {
|
||||
const result = await handleWalletDeletion(walletID);
|
||||
resolve(result);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: loc._.cancel,
|
||||
onPress: () => resolve(false),
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
options: { cancelable: false },
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
[deleteWallet, saveToDisk, wallets],
|
||||
);
|
||||
|
||||
const resetWallets = useCallback(() => {
|
||||
setWallets(BlueApp.getWallets());
|
||||
}, []);
|
||||
|
@ -120,56 +237,72 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
|
|||
}
|
||||
}, [walletsInitialized]);
|
||||
|
||||
// Add a refresh lock to prevent concurrent refreshes
|
||||
const refreshingRef = useRef<boolean>(false);
|
||||
|
||||
const refreshAllWalletTransactions = useCallback(
|
||||
async (lastSnappedTo?: number, showUpdateStatusIndicator: boolean = true) => {
|
||||
const TIMEOUT_DURATION = 30000;
|
||||
if (refreshingRef.current) {
|
||||
console.debug('[refreshAllWalletTransactions] Refresh already in progress');
|
||||
return;
|
||||
}
|
||||
refreshingRef.current = true;
|
||||
|
||||
await new Promise<void>(resolve => InteractionManager.runAfterInteractions(() => resolve()));
|
||||
|
||||
const TIMEOUT_DURATION = 30000;
|
||||
const timeoutPromise = new Promise<never>((_resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
reject(new Error('refreshAllWalletTransactions: Timeout reached'));
|
||||
console.debug('[refreshAllWalletTransactions] Timeout reached');
|
||||
reject(new Error('Timeout reached'));
|
||||
}, TIMEOUT_DURATION),
|
||||
);
|
||||
|
||||
const mainLogicPromise = new Promise<void>((resolve, reject) => {
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
let noErr = true;
|
||||
try {
|
||||
await BlueElectrum.waitTillConnected();
|
||||
if (showUpdateStatusIndicator) {
|
||||
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
|
||||
}
|
||||
const paymentCodesStart = Date.now();
|
||||
await BlueApp.fetchSenderPaymentCodes(lastSnappedTo);
|
||||
const paymentCodesEnd = Date.now();
|
||||
console.debug('fetch payment codes took', (paymentCodesEnd - paymentCodesStart) / 1000, 'sec');
|
||||
try {
|
||||
if (showUpdateStatusIndicator) {
|
||||
console.debug('[refreshAllWalletTransactions] Setting wallet transaction status to ALL');
|
||||
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
|
||||
}
|
||||
console.debug('[refreshAllWalletTransactions] Waiting for connectivity...');
|
||||
await BlueElectrum.waitTillConnected();
|
||||
console.debug('[refreshAllWalletTransactions] Connected to Electrum');
|
||||
|
||||
// Restore fetch payment codes timing measurement
|
||||
if (typeof BlueApp.fetchSenderPaymentCodes === 'function') {
|
||||
const codesStart = Date.now();
|
||||
console.debug('[refreshAllWalletTransactions] Fetching sender payment codes');
|
||||
await BlueApp.fetchSenderPaymentCodes(lastSnappedTo);
|
||||
const codesEnd = Date.now();
|
||||
console.debug('[refreshAllWalletTransactions] fetch payment codes took', (codesEnd - codesStart) / 1000, 'sec');
|
||||
} else {
|
||||
console.warn('[refreshAllWalletTransactions] fetchSenderPaymentCodes is not available');
|
||||
}
|
||||
|
||||
console.debug('[refreshAllWalletTransactions] Fetching wallet balances and transactions');
|
||||
await Promise.race([
|
||||
(async () => {
|
||||
const balanceStart = Date.now();
|
||||
await BlueApp.fetchWalletBalances(lastSnappedTo);
|
||||
const balanceEnd = Date.now();
|
||||
console.debug('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
|
||||
console.debug('[refreshAllWalletTransactions] fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
|
||||
|
||||
const start = Date.now();
|
||||
const txStart = Date.now();
|
||||
await BlueApp.fetchWalletTransactions(lastSnappedTo);
|
||||
const end = Date.now();
|
||||
console.debug('fetch tx took', (end - start) / 1000, 'sec');
|
||||
} catch (err) {
|
||||
noErr = false;
|
||||
console.error(err);
|
||||
reject(err);
|
||||
} finally {
|
||||
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
|
||||
}
|
||||
if (noErr) await saveToDisk();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
const txEnd = Date.now();
|
||||
console.debug('[refreshAllWalletTransactions] fetch tx took', (txEnd - txStart) / 1000, 'sec');
|
||||
|
||||
try {
|
||||
await Promise.race([mainLogicPromise, timeoutPromise]);
|
||||
} catch (err) {
|
||||
console.error('Error in refreshAllWalletTransactions:', err);
|
||||
console.debug('[refreshAllWalletTransactions] Saving data to disk');
|
||||
await saveToDisk();
|
||||
})(),
|
||||
timeoutPromise,
|
||||
]);
|
||||
console.debug('[refreshAllWalletTransactions] Refresh completed successfully');
|
||||
} catch (error) {
|
||||
console.error('[refreshAllWalletTransactions] Error:', error);
|
||||
} finally {
|
||||
console.debug('[refreshAllWalletTransactions] Resetting wallet transaction status and refresh lock');
|
||||
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
|
||||
refreshingRef.current = false;
|
||||
}
|
||||
},
|
||||
[saveToDisk],
|
||||
|
@ -182,24 +315,26 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
|
|||
let noErr = true;
|
||||
try {
|
||||
if (Date.now() - (_lastTimeTriedToRefetchWallet[walletID] || 0) < 5000) {
|
||||
console.debug('Re-fetch wallet happens too fast; NOP');
|
||||
console.debug('[fetchAndSaveWalletTransactions] Re-fetch wallet happens too fast; NOP');
|
||||
return;
|
||||
}
|
||||
_lastTimeTriedToRefetchWallet[walletID] = Date.now();
|
||||
|
||||
await BlueElectrum.waitTillConnected();
|
||||
setWalletTransactionUpdateStatus(walletID);
|
||||
|
||||
const balanceStart = Date.now();
|
||||
await BlueApp.fetchWalletBalances(index);
|
||||
const balanceEnd = Date.now();
|
||||
console.debug('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
|
||||
const start = Date.now();
|
||||
console.debug('[fetchAndSaveWalletTransactions] fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
|
||||
|
||||
const txStart = Date.now();
|
||||
await BlueApp.fetchWalletTransactions(index);
|
||||
const end = Date.now();
|
||||
console.debug('fetch tx took', (end - start) / 1000, 'sec');
|
||||
const txEnd = Date.now();
|
||||
console.debug('[fetchAndSaveWalletTransactions] fetch tx took', (txEnd - txStart) / 1000, 'sec');
|
||||
} catch (err) {
|
||||
noErr = false;
|
||||
console.error(err);
|
||||
console.error('[fetchAndSaveWalletTransactions] Error:', err);
|
||||
} finally {
|
||||
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
|
||||
}
|
||||
|
@ -217,10 +352,10 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
|
|||
return;
|
||||
}
|
||||
const emptyWalletLabel = new LegacyWallet().getLabel();
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
if (w.getLabel() === emptyWalletLabel) w.setLabel(loc.wallets.import_imported + ' ' + w.typeReadable);
|
||||
w.setUserHasSavedExport(true);
|
||||
addWallet(w);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
await saveToDisk();
|
||||
A(A.ENUM.CREATED_WALLET);
|
||||
presentAlert({
|
||||
|
@ -239,6 +374,36 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
|
|||
[wallets, addWallet, saveToDisk],
|
||||
);
|
||||
|
||||
function confirmWalletDeletion(wallet: any, onConfirmed: () => void) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning);
|
||||
try {
|
||||
const balance = formatBalanceWithoutSuffix(wallet.getBalance(), BitcoinUnit.SATS, true);
|
||||
presentAlert({
|
||||
title: loc.wallets.details_delete_wallet,
|
||||
message: loc.formatString(loc.wallets.details_del_wb_q, { balance }),
|
||||
buttons: [
|
||||
{
|
||||
text: loc.wallets.details_delete,
|
||||
onPress: () => {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
onConfirmed();
|
||||
},
|
||||
style: 'destructive',
|
||||
},
|
||||
{
|
||||
text: loc._.cancel,
|
||||
onPress: () => {},
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
options: { cancelable: false },
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle error silently if needed
|
||||
}
|
||||
}
|
||||
|
||||
const value: StorageContextType = useMemo(
|
||||
() => ({
|
||||
wallets,
|
||||
|
@ -274,6 +439,8 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
|
|||
isPasswordInUse: BlueApp.isPasswordInUse,
|
||||
walletTransactionUpdateStatus,
|
||||
setWalletTransactionUpdateStatus,
|
||||
handleWalletDeletion,
|
||||
confirmWalletDeletion,
|
||||
}),
|
||||
[
|
||||
wallets,
|
||||
|
@ -291,7 +458,7 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
|
|||
refreshAllWalletTransactions,
|
||||
resetWallets,
|
||||
walletTransactionUpdateStatus,
|
||||
setWalletTransactionUpdateStatus,
|
||||
handleWalletDeletion,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import React, { forwardRef, ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import React, { forwardRef, ReactNode, useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { Animated, Dimensions, PixelRatio, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { useTheme } from './themes';
|
||||
|
||||
const BORDER_RADIUS = 8;
|
||||
const PADDINGS = 24;
|
||||
const ICON_MARGIN = 7;
|
||||
|
||||
const cStyles = StyleSheet.create({
|
||||
const buttonFontSize = (() => {
|
||||
const baseSize = PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
|
||||
return Math.min(22, baseSize);
|
||||
})();
|
||||
|
||||
const containerStyles = StyleSheet.create({
|
||||
root: {
|
||||
alignSelf: 'center',
|
||||
height: '6.9%',
|
||||
|
@ -26,6 +30,27 @@ const cStyles = StyleSheet.create({
|
|||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
rootRound: {
|
||||
borderRadius: 9999,
|
||||
},
|
||||
});
|
||||
|
||||
const buttonStyles = StyleSheet.create({
|
||||
root: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
icon: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
fontSize: buttonFontSize,
|
||||
fontWeight: '600',
|
||||
marginLeft: ICON_MARGIN,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
interface FContainerProps {
|
||||
|
@ -51,93 +76,79 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
|
|||
}).start();
|
||||
}, [height, slideAnimation]);
|
||||
|
||||
const computeNewWidth = useCallback(
|
||||
(layoutWidth: number, totalChildren: number) => {
|
||||
const maxWidth = width - BORDER_RADIUS - 140;
|
||||
const paddedWidth = Math.ceil(layoutWidth + PADDINGS * 2);
|
||||
let calculatedWidth = paddedWidth * totalChildren > maxWidth ? Math.floor(maxWidth / totalChildren) : paddedWidth;
|
||||
if (totalChildren === 1 && calculatedWidth < 90) calculatedWidth = 90;
|
||||
return calculatedWidth;
|
||||
},
|
||||
[width],
|
||||
);
|
||||
|
||||
const onLayout = (event: { nativeEvent: { layout: { width: number } } }) => {
|
||||
if (layoutCalculated.current) return;
|
||||
const maxWidth = width - BORDER_RADIUS - 140;
|
||||
const layoutWidth = event.nativeEvent.layout.width;
|
||||
const withPaddings = Math.ceil(layoutWidth + PADDINGS * 2);
|
||||
const len = React.Children.toArray(props.children).filter(Boolean).length;
|
||||
let newW = withPaddings * len > maxWidth ? Math.floor(maxWidth / len) : withPaddings;
|
||||
if (len === 1 && newW < 90) newW = 90;
|
||||
setNewWidth(newW);
|
||||
const { width: layoutWidth } = event.nativeEvent.layout;
|
||||
const totalChildren = React.Children.toArray(props.children).filter(Boolean).length;
|
||||
setNewWidth(computeNewWidth(layoutWidth, totalChildren));
|
||||
layoutCalculated.current = true;
|
||||
};
|
||||
|
||||
const renderChild = (child: ReactNode, index: number, array: ReactNode[]): ReactNode => {
|
||||
if (typeof child === 'string') {
|
||||
return (
|
||||
<View key={index} style={{ width: newWidth }}>
|
||||
<Text adjustsFontSizeToFit numberOfLines={1}>
|
||||
{child}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return React.cloneElement(child as React.ReactElement<any>, {
|
||||
width: newWidth,
|
||||
key: index,
|
||||
first: index === 0,
|
||||
last: index === array.length - 1,
|
||||
singleChild: array.length === 1,
|
||||
});
|
||||
};
|
||||
|
||||
const totalChildren = React.Children.toArray(props.children).filter(Boolean).length;
|
||||
return (
|
||||
<Animated.View
|
||||
ref={ref}
|
||||
onLayout={onLayout}
|
||||
style={[
|
||||
cStyles.root,
|
||||
props.inline ? cStyles.rootInline : cStyles.rootAbsolute,
|
||||
containerStyles.root,
|
||||
props.inline ? containerStyles.rootInline : containerStyles.rootAbsolute,
|
||||
bottomInsets,
|
||||
newWidth ? cStyles.rootPost : cStyles.rootPre,
|
||||
newWidth ? containerStyles.rootPost : containerStyles.rootPre,
|
||||
totalChildren === 1 ? containerStyles.rootRound : null,
|
||||
{ transform: [{ translateY: slideAnimation }] },
|
||||
]}
|
||||
>
|
||||
{newWidth
|
||||
? React.Children.toArray(props.children)
|
||||
.filter(Boolean)
|
||||
.map((child, index, array) => {
|
||||
if (typeof child === 'string') {
|
||||
return (
|
||||
<View key={index} style={{ width: newWidth }}>
|
||||
<Text adjustsFontSizeToFit numberOfLines={1}>
|
||||
{child}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return React.cloneElement(child as React.ReactElement<any>, {
|
||||
width: newWidth,
|
||||
key: index,
|
||||
first: index === 0,
|
||||
last: index === array.length - 1,
|
||||
});
|
||||
})
|
||||
: props.children}
|
||||
{newWidth ? React.Children.toArray(props.children).filter(Boolean).map(renderChild) : props.children}
|
||||
</Animated.View>
|
||||
);
|
||||
});
|
||||
|
||||
const buttonFontSize =
|
||||
PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22
|
||||
? 22
|
||||
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
|
||||
|
||||
const bStyles = StyleSheet.create({
|
||||
root: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
icon: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
fontSize: buttonFontSize,
|
||||
fontWeight: '600',
|
||||
marginLeft: ICON_MARGIN,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
interface FButtonProps {
|
||||
text: string;
|
||||
icon: ReactNode;
|
||||
width?: number;
|
||||
first?: boolean;
|
||||
last?: boolean;
|
||||
singleChild?: boolean;
|
||||
disabled?: boolean;
|
||||
testID?: string;
|
||||
onPress: () => void;
|
||||
onLongPress?: () => void;
|
||||
}
|
||||
|
||||
export const FButton = ({ text, icon, width, first, last, testID, ...props }: FButtonProps) => {
|
||||
export const FButton = ({ text, icon, width, first, last, singleChild, testID, ...props }: FButtonProps) => {
|
||||
const { colors } = useTheme();
|
||||
const bStylesHook = StyleSheet.create({
|
||||
const customButtonStyles = StyleSheet.create({
|
||||
root: {
|
||||
backgroundColor: colors.buttonBackgroundColor,
|
||||
borderRadius: BORDER_RADIUS,
|
||||
|
@ -151,9 +162,12 @@ export const FButton = ({ text, icon, width, first, last, testID, ...props }: FB
|
|||
marginRight: {
|
||||
marginRight: 10,
|
||||
},
|
||||
rootRound: {
|
||||
borderRadius: 9999,
|
||||
},
|
||||
});
|
||||
const style: Record<string, any> = {};
|
||||
const additionalStyles = !last ? bStylesHook.marginRight : {};
|
||||
const additionalStyles = !last ? customButtonStyles.marginRight : {};
|
||||
|
||||
if (width) {
|
||||
style.paddingHorizontal = PADDINGS;
|
||||
|
@ -165,11 +179,15 @@ export const FButton = ({ text, icon, width, first, last, testID, ...props }: FB
|
|||
accessibilityLabel={text}
|
||||
accessibilityRole="button"
|
||||
testID={testID}
|
||||
style={[bStyles.root, bStylesHook.root, style, additionalStyles]}
|
||||
style={[buttonStyles.root, customButtonStyles.root, style, additionalStyles, singleChild ? customButtonStyles.rootRound : null]}
|
||||
{...props}
|
||||
>
|
||||
<View style={bStyles.icon}>{icon}</View>
|
||||
<Text numberOfLines={1} adjustsFontSizeToFit style={[bStyles.text, props.disabled ? bStylesHook.textDisabled : bStylesHook.text]}>
|
||||
<View style={buttonStyles.icon}>{icon}</View>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
style={[buttonStyles.text, props.disabled ? customButtonStyles.textDisabled : customButtonStyles.text]}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
|
|
@ -9,7 +9,12 @@ import { HandOffComponentProps } from './types';
|
|||
|
||||
const HandOffComponent: React.FC<HandOffComponentProps> = props => {
|
||||
const { isHandOffUseEnabled } = useSettings();
|
||||
console.debug('HandOffComponent is rendering.');
|
||||
if (!props || !props.type || !props.userInfo || Object.keys(props.userInfo).length === 0) {
|
||||
console.debug('HandOffComponent: Missing required type or userInfo data');
|
||||
return null;
|
||||
}
|
||||
const userInfo = JSON.stringify(props.userInfo);
|
||||
console.debug(`HandOffComponent is rendering. Type: ${props.type}, UserInfo: ${userInfo}...`);
|
||||
return isHandOffUseEnabled ? <Handoff {...props} /> : null;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { View, StyleSheet, ViewStyle, TouchableOpacity, ActivityIndicator } from 'react-native';
|
||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { StyleSheet, ViewStyle, TouchableOpacity, ActivityIndicator, Platform, Animated } from 'react-native';
|
||||
import { Icon, ListItem } from '@rneui/base';
|
||||
import { ExtendedTransaction, LightningTransaction, TWallet } from '../class/wallets/types';
|
||||
import { WalletCarouselItem } from './WalletsCarousel';
|
||||
import { TransactionListItem } from './TransactionListItem';
|
||||
import { useTheme } from './themes';
|
||||
import { BitcoinUnit } from '../models/bitcoinUnits';
|
||||
import { TouchableOpacityWrapper } from './ListItem';
|
||||
import loc from '../loc';
|
||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
|
||||
|
||||
enum ItemType {
|
||||
WalletSection = 'wallet',
|
||||
|
@ -40,6 +40,7 @@ interface ManageWalletsListItemProps {
|
|||
handleToggleHideBalance: (wallet: TWallet) => void;
|
||||
isActive?: boolean;
|
||||
style?: ViewStyle;
|
||||
globalDragActive?: boolean;
|
||||
}
|
||||
|
||||
interface SwipeContentProps {
|
||||
|
@ -66,7 +67,7 @@ const RightSwipeContent: React.FC<Partial<SwipeContentProps>> = ({ onPress }) =>
|
|||
accessibilityRole="button"
|
||||
accessibilityLabel="Delete Wallet"
|
||||
>
|
||||
<Icon name="delete-outline" color="#FFFFFF" />
|
||||
<Icon name={Platform.OS === 'android' ? 'delete' : 'delete-outline'} color="#FFFFFF" />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
|
@ -83,10 +84,35 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
|||
onPressIn,
|
||||
onPressOut,
|
||||
isActive,
|
||||
globalDragActive,
|
||||
style,
|
||||
}) => {
|
||||
const { colors } = useTheme();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSwipeActive, setIsSwipeActive] = useState(false);
|
||||
const resetFunctionRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const CARD_SORT_ACTIVE = 1.06;
|
||||
const INACTIVE_SCALE_WHEN_ACTIVE = 0.9;
|
||||
const SCALE_DURATION = 200;
|
||||
const scaleValue = useRef(new Animated.Value(1)).current;
|
||||
const prevIsActive = useRef(isActive);
|
||||
|
||||
const DEFAULT_VERTICAL_MARGIN = -10;
|
||||
const REDUCED_VERTICAL_MARGIN = -50;
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive !== prevIsActive.current) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
|
||||
}
|
||||
prevIsActive.current = isActive;
|
||||
|
||||
Animated.timing(scaleValue, {
|
||||
toValue: isActive ? CARD_SORT_ACTIVE : globalDragActive ? INACTIVE_SCALE_WHEN_ACTIVE : 1,
|
||||
duration: SCALE_DURATION,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, [isActive, globalDragActive, scaleValue]);
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
if (item.type === ItemType.WalletSection) {
|
||||
|
@ -101,43 +127,83 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
|||
reset();
|
||||
};
|
||||
|
||||
const leftContent = (reset: () => void) => (
|
||||
<LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />
|
||||
);
|
||||
|
||||
const handleRightPress = (reset: () => void) => {
|
||||
handleDeleteWallet(item.data as TWallet);
|
||||
reset();
|
||||
const leftContent = (reset: () => void) => {
|
||||
resetFunctionRef.current = reset;
|
||||
return <LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />;
|
||||
};
|
||||
|
||||
const rightContent = (reset: () => void) => <RightSwipeContent onPress={() => handleRightPress(reset)} />;
|
||||
const handleRightPress = (reset: () => void) => {
|
||||
reset();
|
||||
|
||||
setTimeout(() => {
|
||||
handleDeleteWallet(item.data as TWallet);
|
||||
}, 100); // short delay to allow swipe reset animation to complete
|
||||
};
|
||||
|
||||
const rightContent = (reset: () => void) => {
|
||||
resetFunctionRef.current = reset;
|
||||
return <RightSwipeContent onPress={() => handleRightPress(reset)} />;
|
||||
};
|
||||
|
||||
const startDrag = useCallback(() => {
|
||||
if (isSwipeActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resetFunctionRef.current) {
|
||||
resetFunctionRef.current();
|
||||
}
|
||||
|
||||
scaleValue.setValue(CARD_SORT_ACTIVE);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
|
||||
if (drag) {
|
||||
drag();
|
||||
}
|
||||
}, [CARD_SORT_ACTIVE, drag, scaleValue, isSwipeActive]);
|
||||
|
||||
if (isLoading) {
|
||||
return <ActivityIndicator size="large" color={colors.brandingColor} />;
|
||||
}
|
||||
|
||||
if (item.type === ItemType.WalletSection) {
|
||||
const animatedStyle = {
|
||||
transform: [{ scale: scaleValue }],
|
||||
marginVertical: globalDragActive && !isActive ? REDUCED_VERTICAL_MARGIN : DEFAULT_VERTICAL_MARGIN,
|
||||
};
|
||||
|
||||
const backgroundColor = isActive || globalDragActive ? colors.brandingColor : colors.background;
|
||||
|
||||
const swipeDisabled = isActive || globalDragActive;
|
||||
|
||||
return (
|
||||
<ListItem.Swipeable
|
||||
leftWidth={80}
|
||||
rightWidth={90}
|
||||
containerStyle={[{ backgroundColor: colors.background }, style]}
|
||||
leftContent={leftContent}
|
||||
rightContent={rightContent}
|
||||
Component={TouchableOpacityWrapper}
|
||||
onPressOut={onPressOut}
|
||||
onPressIn={onPressIn}
|
||||
style={isActive ? styles.activeItem : undefined}
|
||||
>
|
||||
<ListItem.Content
|
||||
style={{
|
||||
backgroundColor: colors.background,
|
||||
<Animated.View style={animatedStyle}>
|
||||
<ListItem.Swipeable
|
||||
leftWidth={swipeDisabled ? 0 : 80}
|
||||
rightWidth={swipeDisabled ? 0 : 90}
|
||||
containerStyle={[style, { backgroundColor }, swipeDisabled ? styles.transparentBackground : {}]}
|
||||
leftContent={swipeDisabled ? null : leftContent}
|
||||
rightContent={swipeDisabled ? null : rightContent}
|
||||
onPressOut={onPressOut}
|
||||
minSlideWidth={swipeDisabled ? 0 : 80}
|
||||
onPressIn={onPressIn}
|
||||
style={swipeDisabled ? styles.transparentBackground : {}}
|
||||
onSwipeBegin={direction => {
|
||||
if (!swipeDisabled) {
|
||||
console.debug(`Swipe began: ${direction}`);
|
||||
setIsSwipeActive(true);
|
||||
}
|
||||
}}
|
||||
onSwipeEnd={() => {
|
||||
if (!swipeDisabled) {
|
||||
console.debug('Swipe ended');
|
||||
setIsSwipeActive(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View style={styles.walletCarouselItemContainer}>
|
||||
<ListItem.Content>
|
||||
<WalletCarouselItem
|
||||
item={item.data}
|
||||
handleLongPress={isDraggingDisabled ? undefined : drag}
|
||||
handleLongPress={isDraggingDisabled || isSwipeActive ? undefined : startDrag}
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}
|
||||
|
@ -145,10 +211,11 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
|||
searchQuery={state.searchQuery}
|
||||
isPlaceHolder={isPlaceHolder}
|
||||
renderHighlightedText={renderHighlightedText}
|
||||
customStyle={styles.carouselItem}
|
||||
/>
|
||||
</View>
|
||||
</ListItem.Content>
|
||||
</ListItem.Swipeable>
|
||||
</ListItem.Content>
|
||||
</ListItem.Swipeable>
|
||||
</Animated.View>
|
||||
);
|
||||
} else if (item.type === ItemType.TransactionSection && item.data) {
|
||||
const w = state.wallets.find(wallet => wallet.getTransactions().some((tx: ExtendedTransaction) => tx.hash === item.data.hash));
|
||||
|
@ -169,22 +236,22 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
|||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
walletCarouselItemContainer: {
|
||||
width: '100%',
|
||||
},
|
||||
leftButtonContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
carouselItem: {
|
||||
width: '100%',
|
||||
},
|
||||
rightButtonContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'red',
|
||||
},
|
||||
activeItem: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
||||
transparentBackground: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,23 +1,66 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { useRef } from 'react';
|
||||
import { ActivityIndicator, findNodeHandle, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
findNodeHandle,
|
||||
GestureResponderEvent,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { Icon } from '@rneui/themed';
|
||||
|
||||
import ActionSheet from '../screen/ActionSheet';
|
||||
import ActionSheetOptions from '../screen/ActionSheet.common';
|
||||
import { useTheme } from './themes';
|
||||
export const MultipleStepsListItemDashType = Object.freeze({ none: 0, top: 1, bottom: 2, topAndBottom: 3 });
|
||||
export const MultipleStepsListItemButtohType = Object.freeze({ partial: 0, full: 1 });
|
||||
import { ActionSheetOptions } from '../screen/ActionSheet.common';
|
||||
|
||||
const MultipleStepsListItem = props => {
|
||||
export enum MultipleStepsListItemDashType {
|
||||
None = 0,
|
||||
Top = 1,
|
||||
Bottom = 2,
|
||||
TopAndBottom = 3,
|
||||
}
|
||||
|
||||
export enum MultipleStepsListItemButtonType {
|
||||
Partial = 0,
|
||||
Full = 1,
|
||||
}
|
||||
|
||||
interface MultipleStepsListItemProps {
|
||||
circledText?: string;
|
||||
checked?: boolean;
|
||||
leftText?: string;
|
||||
showActivityIndicator?: boolean;
|
||||
isActionSheet?: boolean;
|
||||
actionSheetOptions?: ActionSheetOptions;
|
||||
dashes?: MultipleStepsListItemDashType;
|
||||
button?: {
|
||||
text?: string;
|
||||
onPress?: (e: GestureResponderEvent | number) => void;
|
||||
disabled?: boolean;
|
||||
buttonType?: MultipleStepsListItemButtonType;
|
||||
leftText?: string;
|
||||
showActivityIndicator?: boolean;
|
||||
testID?: string;
|
||||
};
|
||||
rightButton?: {
|
||||
text?: string;
|
||||
onPress?: () => void;
|
||||
disabled?: boolean;
|
||||
showActivityIndicator?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const MultipleStepsListItem = (props: MultipleStepsListItemProps) => {
|
||||
const { colors } = useTheme();
|
||||
const {
|
||||
showActivityIndicator = false,
|
||||
dashes = MultipleStepsListItemDashType.none,
|
||||
dashes = MultipleStepsListItemDashType.None,
|
||||
circledText = '',
|
||||
leftText = '',
|
||||
checked = false,
|
||||
useActionSheet = false,
|
||||
isActionSheet = false,
|
||||
actionSheetOptions = null, // Default to null or appropriate default
|
||||
} = props;
|
||||
const stylesHook = StyleSheet.create({
|
||||
|
@ -43,7 +86,7 @@ const MultipleStepsListItem = props => {
|
|||
const selfRef = useRef(null); // Create a ref for the component itself
|
||||
|
||||
const handleOnPressForActionSheet = () => {
|
||||
if (useActionSheet && actionSheetOptions) {
|
||||
if (isActionSheet && actionSheetOptions) {
|
||||
// Clone options to modify them
|
||||
let modifiedOptions = { ...actionSheetOptions };
|
||||
|
||||
|
@ -57,16 +100,16 @@ const MultipleStepsListItem = props => {
|
|||
|
||||
ActionSheet.showActionSheetWithOptions(modifiedOptions, buttonIndex => {
|
||||
// Call the original onPress function, if provided, and not cancelled
|
||||
if (buttonIndex !== -1 && props.button.onPress) {
|
||||
if (buttonIndex !== -1 && props.button?.onPress) {
|
||||
props.button.onPress(buttonIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderDashes = () => {
|
||||
const renderDashes = (): StyleProp<ViewStyle> => {
|
||||
switch (dashes) {
|
||||
case MultipleStepsListItemDashType.topAndBottom:
|
||||
case MultipleStepsListItemDashType.TopAndBottom:
|
||||
return {
|
||||
width: 1,
|
||||
borderStyle: 'dashed',
|
||||
|
@ -77,7 +120,7 @@ const MultipleStepsListItem = props => {
|
|||
marginLeft: 20,
|
||||
position: 'absolute',
|
||||
};
|
||||
case MultipleStepsListItemDashType.bottom:
|
||||
case MultipleStepsListItemDashType.Bottom:
|
||||
return {
|
||||
width: 1,
|
||||
borderStyle: 'dashed',
|
||||
|
@ -88,7 +131,7 @@ const MultipleStepsListItem = props => {
|
|||
marginLeft: 20,
|
||||
position: 'absolute',
|
||||
};
|
||||
case MultipleStepsListItemDashType.top:
|
||||
case MultipleStepsListItemDashType.Top:
|
||||
return {
|
||||
width: 1,
|
||||
borderStyle: 'dashed',
|
||||
|
@ -105,6 +148,7 @@ const MultipleStepsListItem = props => {
|
|||
};
|
||||
const buttonOpacity = { opacity: props.button?.disabled ? 0.5 : 1.0 };
|
||||
const rightButtonOpacity = { opacity: props.rightButton?.disabled ? 0.5 : 1.0 };
|
||||
const onPress = isActionSheet ? handleOnPressForActionSheet : props.button?.onPress;
|
||||
return (
|
||||
<View>
|
||||
<View style={renderDashes()} />
|
||||
|
@ -131,19 +175,19 @@ const MultipleStepsListItem = props => {
|
|||
{!showActivityIndicator && props.button && (
|
||||
<>
|
||||
{props.button.buttonType === undefined ||
|
||||
(props.button.buttonType === MultipleStepsListItemButtohType.full && (
|
||||
(props.button.buttonType === MultipleStepsListItemButtonType.Full && (
|
||||
<TouchableOpacity
|
||||
ref={useActionSheet ? selfRef : null}
|
||||
ref={isActionSheet ? selfRef : null}
|
||||
testID={props.button.testID}
|
||||
accessibilityRole="button"
|
||||
disabled={props.button.disabled}
|
||||
style={[styles.provideKeyButton, stylesHook.provideKeyButton, buttonOpacity]}
|
||||
onPress={useActionSheet ? handleOnPressForActionSheet : props.button.onPress}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.button.text}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
{props.button.buttonType === MultipleStepsListItemButtohType.partial && (
|
||||
{props.button.buttonType === MultipleStepsListItemButtonType.Partial && (
|
||||
<View style={styles.buttonPartialContainer}>
|
||||
<Text numberOfLines={1} style={[styles.rowPartialLeftText, stylesHook.rowPartialLeftText]} lineBreakMode="middle">
|
||||
{props.button.leftText}
|
||||
|
@ -153,7 +197,7 @@ const MultipleStepsListItem = props => {
|
|||
accessibilityRole="button"
|
||||
disabled={props.button.disabled}
|
||||
style={[styles.rowPartialRightButton, stylesHook.provideKeyButton, rightButtonOpacity]}
|
||||
onPress={props.button.onPress}
|
||||
onPress={onPress}
|
||||
>
|
||||
{props.button.showActivityIndicator ? (
|
||||
<ActivityIndicator />
|
||||
|
@ -188,30 +232,6 @@ const MultipleStepsListItem = props => {
|
|||
);
|
||||
};
|
||||
|
||||
MultipleStepsListItem.propTypes = {
|
||||
circledText: PropTypes.string,
|
||||
checked: PropTypes.bool,
|
||||
leftText: PropTypes.string,
|
||||
showActivityIndicator: PropTypes.bool,
|
||||
useActionSheet: PropTypes.bool,
|
||||
actionSheetOptions: PropTypes.shape(ActionSheetOptions),
|
||||
dashes: PropTypes.number,
|
||||
button: PropTypes.shape({
|
||||
text: PropTypes.string,
|
||||
onPress: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
buttonType: PropTypes.number,
|
||||
leftText: PropTypes.string,
|
||||
showActivityIndicator: PropTypes.bool,
|
||||
}),
|
||||
rightButton: PropTypes.shape({
|
||||
text: PropTypes.string,
|
||||
onPress: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
showActivityIndicator: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useRef, forwardRef, useImperativeHandle, useEffect } from 'react';
|
||||
import { View, Text, TextInput, StyleSheet, Animated, Easing, ViewStyle, Keyboard, Platform, UIManager, ScrollView } from 'react-native';
|
||||
import { View, Text, TextInput, StyleSheet, Animated, Easing, ViewStyle, Keyboard, Platform, UIManager } from 'react-native';
|
||||
import BottomModal, { BottomModalHandle } from './BottomModal';
|
||||
import { useTheme } from '../components/themes';
|
||||
import loc from '../loc';
|
||||
|
@ -43,11 +43,10 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
const fadeInAnimation = useRef(new Animated.Value(0)).current;
|
||||
const scaleAnimation = useRef(new Animated.Value(1)).current;
|
||||
const shakeAnimation = useRef(new Animated.Value(0)).current;
|
||||
const explanationOpacity = useRef(new Animated.Value(1)).current; // New animated value for opacity
|
||||
const explanationOpacity = useRef(new Animated.Value(1)).current;
|
||||
const { colors } = useTheme();
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const confirmPasswordInputRef = useRef<TextInput>(null);
|
||||
const scrollView = useRef<ScrollView>(null);
|
||||
const { isVisible } = useKeyboard();
|
||||
|
||||
const stylesHook = StyleSheet.create({
|
||||
|
@ -103,42 +102,43 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [modalType]);
|
||||
|
||||
const handleShakeAnimation = () => {
|
||||
const performShake = (shakeAnimRef: Animated.Value) => {
|
||||
Animated.sequence([
|
||||
Animated.timing(shakeAnimation, {
|
||||
Animated.timing(shakeAnimRef, {
|
||||
toValue: 10,
|
||||
duration: 100,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(shakeAnimation, {
|
||||
Animated.timing(shakeAnimRef, {
|
||||
toValue: -10,
|
||||
duration: 100,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(shakeAnimation, {
|
||||
Animated.timing(shakeAnimRef, {
|
||||
toValue: 5,
|
||||
duration: 100,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(shakeAnimation, {
|
||||
Animated.timing(shakeAnimRef, {
|
||||
toValue: -5,
|
||||
duration: 100,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(shakeAnimation, {
|
||||
Animated.timing(shakeAnimRef, {
|
||||
toValue: 0,
|
||||
duration: 100,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
confirmPasswordInputRef.current?.setNativeProps({ selection: { start: 0, end: confirmPassword.length } });
|
||||
});
|
||||
]).start();
|
||||
};
|
||||
|
||||
const handleShakeAnimation = () => {
|
||||
performShake(shakeAnimation);
|
||||
};
|
||||
|
||||
const handleSuccessAnimation = () => {
|
||||
|
@ -180,6 +180,17 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
});
|
||||
};
|
||||
|
||||
const handleConfirmationFailure = () => {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
if (!isSuccess) handleShakeAnimation();
|
||||
onConfirmationFailure();
|
||||
};
|
||||
|
||||
const handleConfirmSuccess = () => {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
handleSuccessAnimation();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
Keyboard.dismiss();
|
||||
setIsLoading(true);
|
||||
|
@ -189,37 +200,13 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
if (modalType === MODAL_TYPES.CREATE_PASSWORD || modalType === MODAL_TYPES.CREATE_FAKE_STORAGE) {
|
||||
if (password === confirmPassword && password) {
|
||||
success = await onConfirmationSuccess(password);
|
||||
if (success) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
handleSuccessAnimation();
|
||||
} else {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
onConfirmationFailure();
|
||||
if (!isSuccess) {
|
||||
// Prevent shake animation if success is detected
|
||||
handleShakeAnimation();
|
||||
}
|
||||
}
|
||||
success ? handleConfirmSuccess() : handleConfirmationFailure();
|
||||
} else {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
if (!isSuccess) {
|
||||
// Prevent shake animation if success is detected
|
||||
handleShakeAnimation();
|
||||
}
|
||||
handleConfirmationFailure();
|
||||
}
|
||||
} else if (modalType === MODAL_TYPES.ENTER_PASSWORD) {
|
||||
success = await onConfirmationSuccess(password);
|
||||
if (success) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
handleSuccessAnimation();
|
||||
} else {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
if (!isSuccess) {
|
||||
// Prevent shake animation if success is detected
|
||||
handleShakeAnimation();
|
||||
}
|
||||
onConfirmationFailure();
|
||||
}
|
||||
success ? handleConfirmSuccess() : handleConfirmationFailure();
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false); // Ensure loading state is reset
|
||||
|
@ -258,18 +245,18 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
onConfirmationFailure();
|
||||
};
|
||||
|
||||
const opacity = isVisible ? 0 : 1;
|
||||
return (
|
||||
<BottomModal
|
||||
ref={modalRef}
|
||||
onDismiss={onModalDismiss}
|
||||
onClose={onModalDismiss}
|
||||
grabber={false}
|
||||
showCloseButton={!isSuccess}
|
||||
onCloseModalPressed={handleCancel}
|
||||
backgroundColor={colors.modal}
|
||||
isGrabberVisible={!isSuccess}
|
||||
scrollRef={scrollView}
|
||||
keyboardMode="pan"
|
||||
dismissible={false}
|
||||
sizes={Platform.OS === 'ios' ? ['auto'] : [420, 'auto']}
|
||||
footer={
|
||||
!isSuccess ? (
|
||||
showExplanation && modalType === MODAL_TYPES.CREATE_PASSWORD ? (
|
||||
|
@ -282,16 +269,19 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
/>
|
||||
</Animated.View>
|
||||
) : (
|
||||
<Animated.View style={[{ opacity: fadeOutAnimation, transform: [{ scale: scaleAnimation }] }, styles.feeModalFooter]}>
|
||||
{!isVisible && (
|
||||
<SecondButton
|
||||
title={isLoading ? '' : loc._.ok}
|
||||
onPress={handleSubmit}
|
||||
testID="OKButton"
|
||||
loading={isLoading}
|
||||
disabled={isLoading || !password || (modalType === MODAL_TYPES.CREATE_PASSWORD && !confirmPassword)}
|
||||
/>
|
||||
)}
|
||||
<Animated.View
|
||||
style={[
|
||||
{ opacity: isVisible ? opacity : fadeOutAnimation, transform: [{ scale: scaleAnimation }] },
|
||||
styles.feeModalFooterSpacing,
|
||||
]}
|
||||
>
|
||||
<SecondButton
|
||||
title={isLoading ? '' : loc._.ok}
|
||||
onPress={handleSubmit}
|
||||
testID="OKButton"
|
||||
loading={isLoading}
|
||||
disabled={isLoading || !password || (modalType === MODAL_TYPES.CREATE_PASSWORD && !confirmPassword)}
|
||||
/>
|
||||
</Animated.View>
|
||||
)
|
||||
) : null
|
||||
|
@ -302,14 +292,14 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
{modalType === MODAL_TYPES.CREATE_PASSWORD && showExplanation && (
|
||||
<Animated.View style={{ opacity: explanationOpacity }}>
|
||||
<Text style={[styles.textLabel, stylesHook.feeModalLabel]}>{loc.settings.encrypt_storage_explanation_headline}</Text>
|
||||
<Animated.ScrollView style={styles.explanationScrollView} ref={scrollView}>
|
||||
<Text style={[styles.description, stylesHook.feeModalCustomText]}>
|
||||
<Animated.View>
|
||||
<Text style={[styles.description, stylesHook.feeModalCustomText]} maxFontSizeMultiplier={1.2}>
|
||||
{loc.settings.encrypt_storage_explanation_description_line1}
|
||||
</Text>
|
||||
<Text style={[styles.description, stylesHook.feeModalCustomText]}>
|
||||
<Text style={[styles.description, stylesHook.feeModalCustomText]} maxFontSizeMultiplier={1.2}>
|
||||
{loc.settings.encrypt_storage_explanation_description_line2}
|
||||
</Text>
|
||||
</Animated.ScrollView>
|
||||
</Animated.View>
|
||||
<View style={styles.feeModalFooter} />
|
||||
</Animated.View>
|
||||
)}
|
||||
|
@ -398,29 +388,30 @@ export default PromptPasswordConfirmationModal;
|
|||
const styles = StyleSheet.create({
|
||||
modalContent: {
|
||||
padding: 22,
|
||||
width: '100%', // Ensure modal content takes full width
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
minHeight: {
|
||||
minHeight: 280,
|
||||
minHeight: 420,
|
||||
},
|
||||
feeModalFooter: {
|
||||
padding: 16,
|
||||
},
|
||||
feeModalFooterSpacing: {
|
||||
padding: 16,
|
||||
padding: 24,
|
||||
marginVertical: 24,
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 10,
|
||||
width: '100%', // Ensure full width
|
||||
width: '100%',
|
||||
},
|
||||
input: {
|
||||
borderRadius: 4,
|
||||
padding: 8,
|
||||
marginVertical: 8,
|
||||
fontSize: 16,
|
||||
width: '100%', // Ensure full width
|
||||
width: '100%',
|
||||
},
|
||||
textLabel: {
|
||||
fontSize: 20,
|
||||
|
@ -436,7 +427,8 @@ const styles = StyleSheet.create({
|
|||
successContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: 100,
|
||||
margin: 24,
|
||||
marginBottom: 48,
|
||||
},
|
||||
circle: {
|
||||
width: 60,
|
||||
|
@ -450,7 +442,4 @@ const styles = StyleSheet.create({
|
|||
color: 'white',
|
||||
fontSize: 30,
|
||||
},
|
||||
explanationScrollView: {
|
||||
maxHeight: 200,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { requireNativeComponent, View, StyleSheet, NativeSyntheticEvent } from 'react-native';
|
||||
|
||||
interface SegmentedControlProps {
|
||||
|
@ -21,9 +21,18 @@ interface NativeSegmentedControlProps {
|
|||
const NativeSegmentedControl = requireNativeComponent<NativeSegmentedControlProps>('CustomSegmentedControl');
|
||||
|
||||
const SegmentedControl: React.FC<SegmentedControlProps> = ({ values, selectedIndex, onChange }) => {
|
||||
const handleChange = (event: NativeSyntheticEvent<SegmentedControlEvent>) => {
|
||||
onChange(event.nativeEvent.selectedIndex);
|
||||
};
|
||||
const handleChange = useMemo(
|
||||
() => (event: NativeSyntheticEvent<SegmentedControlEvent>) => {
|
||||
if (event?.nativeEvent?.selectedIndex !== undefined) {
|
||||
onChange(event.nativeEvent.selectedIndex);
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
if (!Array.isArray(values) || values.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
|
|
@ -109,8 +109,8 @@ const SelectFeeModal = forwardRef<BottomModalHandle, SelectFeeModalProps>(
|
|||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
present: async () => feeModalRef.current?.present(),
|
||||
dismiss: async () => feeModalRef.current?.dismiss(),
|
||||
present: async () => await feeModalRef.current?.present(),
|
||||
dismiss: async () => await feeModalRef.current?.dismiss(),
|
||||
}));
|
||||
|
||||
const options: Option[] = [
|
||||
|
@ -163,8 +163,8 @@ const SelectFeeModal = forwardRef<BottomModalHandle, SelectFeeModalProps>(
|
|||
|
||||
const handleSelectOption = async (fee: number | null, rate: number) => {
|
||||
setFeePrecalc(fp => ({ ...fp, current: fee }));
|
||||
await feeModalRef.current?.dismiss();
|
||||
setCustomFee(rate.toString());
|
||||
await feeModalRef.current?.dismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
73
components/TipBox.tsx
Normal file
73
components/TipBox.tsx
Normal 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;
|
|
@ -1,26 +1,15 @@
|
|||
import React, { Ref, useCallback, useMemo } from 'react';
|
||||
import { Platform, Pressable, TouchableOpacity } from 'react-native';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Platform, TouchableOpacity } from 'react-native';
|
||||
import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu';
|
||||
import {
|
||||
ContextMenuView,
|
||||
RenderItem,
|
||||
OnPressMenuItemEventObject,
|
||||
MenuState,
|
||||
IconConfig,
|
||||
MenuElementConfig,
|
||||
} from 'react-native-ios-context-menu';
|
||||
import { ToolTipMenuProps, Action } from './types';
|
||||
import { useSettings } from '../hooks/context/useSettings';
|
||||
|
||||
const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
||||
const ToolTipMenu = (props: ToolTipMenuProps) => {
|
||||
const {
|
||||
title = '',
|
||||
isMenuPrimaryAction = false,
|
||||
renderPreview,
|
||||
disabled = false,
|
||||
onPress,
|
||||
onMenuWillShow,
|
||||
onMenuWillHide,
|
||||
buttonStyle,
|
||||
onPressMenuItem,
|
||||
children,
|
||||
|
@ -30,58 +19,59 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
|||
|
||||
const { language } = useSettings();
|
||||
|
||||
// Map Menu Items for iOS Context Menu
|
||||
const mapMenuItemForContextMenuView = useCallback((action: Action) => {
|
||||
if (!action.id) return null;
|
||||
return {
|
||||
actionKey: action.id.toString(),
|
||||
actionTitle: action.text,
|
||||
icon: action.icon?.iconValue ? ({ iconType: 'SYSTEM', iconValue: action.icon.iconValue } as IconConfig) : undefined,
|
||||
state: action.menuState ?? undefined,
|
||||
attributes: action.disabled ? ['disabled'] : [],
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Map Menu Items for RN Menu (supports subactions and displayInline)
|
||||
const mapMenuItemForMenuView = useCallback((action: Action): MenuAction | null => {
|
||||
if (!action.id) return null;
|
||||
|
||||
// Check for subactions
|
||||
const subactions =
|
||||
action.subactions?.map(subaction => ({
|
||||
id: subaction.id.toString(),
|
||||
title: subaction.text,
|
||||
subtitle: subaction.subtitle,
|
||||
image: subaction.icon?.iconValue ? subaction.icon.iconValue : undefined,
|
||||
state: subaction.menuState === undefined ? undefined : ((subaction.menuState ? 'on' : 'off') as MenuState),
|
||||
attributes: { disabled: subaction.disabled, destructive: subaction.destructive, hidden: subaction.hidden },
|
||||
subactions: subaction.subactions?.map(subsubaction => ({
|
||||
id: subsubaction.id.toString(),
|
||||
title: subsubaction.text,
|
||||
subtitle: subsubaction.subtitle,
|
||||
image: subsubaction.icon?.iconValue ? subsubaction.icon.iconValue : undefined,
|
||||
state: subsubaction.menuState === undefined ? undefined : ((subsubaction.menuState ? 'on' : 'off') as MenuState),
|
||||
attributes: { disabled: subsubaction.disabled, destructive: subsubaction.destructive, hidden: subsubaction.hidden },
|
||||
})),
|
||||
})) || [];
|
||||
action.subactions?.map(subaction => {
|
||||
const subMenuItem: MenuAction = {
|
||||
id: subaction.id.toString(),
|
||||
title: subaction.text,
|
||||
subtitle: subaction.subtitle,
|
||||
image: subaction.icon?.iconValue ? subaction.icon.iconValue : undefined,
|
||||
attributes: { disabled: subaction.disabled, destructive: subaction.destructive, hidden: subaction.hidden },
|
||||
};
|
||||
if ('menuState' in subaction) {
|
||||
subMenuItem.state = subaction.menuState ? 'on' : 'off';
|
||||
}
|
||||
if (subaction.subactions && subaction.subactions.length > 0) {
|
||||
const deepSubactions = subaction.subactions.map(deepSub => {
|
||||
const deepMenuItem: MenuAction = {
|
||||
id: deepSub.id.toString(),
|
||||
title: deepSub.text,
|
||||
subtitle: deepSub.subtitle,
|
||||
image: deepSub.icon?.iconValue ? deepSub.icon.iconValue : undefined,
|
||||
attributes: { disabled: deepSub.disabled, destructive: deepSub.destructive, hidden: deepSub.hidden },
|
||||
};
|
||||
if ('menuState' in deepSub) {
|
||||
deepMenuItem.state = deepSub.menuState ? 'on' : 'off';
|
||||
}
|
||||
return deepMenuItem;
|
||||
});
|
||||
subMenuItem.subactions = deepSubactions;
|
||||
}
|
||||
return subMenuItem;
|
||||
}) || [];
|
||||
|
||||
return {
|
||||
const menuItem: MenuAction = {
|
||||
id: action.id.toString(),
|
||||
title: action.text,
|
||||
subtitle: action.subtitle,
|
||||
image: action.icon?.iconValue ? action.icon.iconValue : undefined,
|
||||
state: action.menuState === undefined ? undefined : ((action.menuState ? 'on' : 'off') as MenuState),
|
||||
attributes: { disabled: action.disabled, destructive: action.destructive, hidden: action.hidden },
|
||||
subactions: subactions.length > 0 ? subactions : undefined,
|
||||
displayInline: action.displayInline || false,
|
||||
};
|
||||
if ('menuState' in action) {
|
||||
menuItem.state = action.menuState ? 'on' : 'off';
|
||||
}
|
||||
if (subactions.length > 0) {
|
||||
menuItem.subactions = subactions;
|
||||
}
|
||||
return menuItem;
|
||||
}, []);
|
||||
|
||||
const contextMenuItems = useMemo(() => {
|
||||
const flattenedActions = props.actions.flat().filter(action => action.id);
|
||||
return flattenedActions.map(mapMenuItemForContextMenuView).filter(item => item !== null) as MenuElementConfig[];
|
||||
}, [props.actions, mapMenuItemForContextMenuView]);
|
||||
|
||||
const menuViewItemsIOS = useMemo(() => {
|
||||
return props.actions
|
||||
.map(actionGroup => {
|
||||
|
@ -108,13 +98,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
|||
return mergedActions.map(mapMenuItemForMenuView).filter(item => item !== null) as MenuAction[];
|
||||
}, [props.actions, mapMenuItemForMenuView]);
|
||||
|
||||
const handlePressMenuItemForContextMenuView = useCallback(
|
||||
(event: OnPressMenuItemEventObject) => {
|
||||
onPressMenuItem(event.nativeEvent.actionKey);
|
||||
},
|
||||
[onPressMenuItem],
|
||||
);
|
||||
|
||||
const handlePressMenuItemForMenuView = useCallback(
|
||||
({ nativeEvent }: NativeActionEvent) => {
|
||||
onPressMenuItem(nativeEvent.event);
|
||||
|
@ -122,46 +105,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
|||
[onPressMenuItem],
|
||||
);
|
||||
|
||||
const renderContextMenuView = () => {
|
||||
return (
|
||||
<ContextMenuView
|
||||
lazyPreview
|
||||
accessibilityLabel={props.accessibilityLabel}
|
||||
accessibilityHint={props.accessibilityHint}
|
||||
accessibilityRole={props.accessibilityRole}
|
||||
accessibilityState={props.accessibilityState}
|
||||
accessibilityLanguage={language}
|
||||
shouldEnableAggressiveCleanup
|
||||
internalCleanupMode="automatic"
|
||||
onPressMenuItem={handlePressMenuItemForContextMenuView}
|
||||
onMenuWillShow={onMenuWillShow}
|
||||
onMenuWillHide={onMenuWillHide}
|
||||
useActionSheetFallback={false}
|
||||
menuConfig={{
|
||||
menuTitle: title,
|
||||
menuItems: contextMenuItems,
|
||||
}}
|
||||
{...(renderPreview
|
||||
? {
|
||||
previewConfig: {
|
||||
previewType: 'CUSTOM',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
renderPreview: renderPreview as RenderItem,
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
{onPress ? (
|
||||
<Pressable accessibilityRole="button" onPress={onPress} {...restProps}>
|
||||
{children}
|
||||
</Pressable>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</ContextMenuView>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMenuView = () => {
|
||||
return (
|
||||
<MenuView
|
||||
|
@ -187,7 +130,7 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
|||
);
|
||||
};
|
||||
|
||||
return props.actions.length > 0 ? (Platform.OS === 'ios' && renderPreview ? renderContextMenuView() : renderMenuView()) : null;
|
||||
});
|
||||
return props.actions.length > 0 ? renderMenuView() : null;
|
||||
};
|
||||
|
||||
export default ToolTipMenu;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState, memo } from 'react';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import { Linking, View, ViewStyle } from 'react-native';
|
||||
|
@ -36,7 +36,7 @@ interface TransactionListItemProps {
|
|||
|
||||
type NavigationProps = NativeStackNavigationProp<DetailViewStackParamList>;
|
||||
|
||||
export const TransactionListItem: React.FC<TransactionListItemProps> = React.memo(
|
||||
export const TransactionListItem: React.FC<TransactionListItemProps> = memo(
|
||||
({ item, itemPriceUnit = BitcoinUnit.BTC, walletID, searchQuery, style, renderHighlightedText }) => {
|
||||
const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1);
|
||||
const { colors } = useTheme();
|
||||
|
@ -46,10 +46,10 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
const { language, selectedBlockExplorer } = useSettings();
|
||||
const containerStyle = useMemo(
|
||||
() => ({
|
||||
backgroundColor: 'transparent',
|
||||
backgroundColor: colors.background,
|
||||
borderBottomColor: colors.lightBorder,
|
||||
}),
|
||||
[colors.lightBorder],
|
||||
[colors.background, colors.lightBorder],
|
||||
);
|
||||
|
||||
const combinedStyle = useMemo(() => [containerStyle, style], [containerStyle, style]);
|
||||
|
@ -81,28 +81,23 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
return sub || undefined;
|
||||
}, [txMemo, item.confirmations, item.memo]);
|
||||
|
||||
const formattedAmount = useMemo(() => {
|
||||
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
|
||||
}, [item.value, itemPriceUnit]);
|
||||
|
||||
const rowTitle = useMemo(() => {
|
||||
if (item.type === 'user_invoice' || item.type === 'payment_request') {
|
||||
if (isNaN(Number(item.value))) {
|
||||
item.value = 0;
|
||||
}
|
||||
const currentDate = new Date();
|
||||
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
|
||||
const now = Math.floor(currentDate.getTime() / 1000);
|
||||
const invoiceExpiration = item.timestamp! + item.expire_time!;
|
||||
|
||||
if (invoiceExpiration > now) {
|
||||
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
|
||||
if (invoiceExpiration > now || item.ispaid) {
|
||||
return formattedAmount;
|
||||
} else {
|
||||
if (item.ispaid) {
|
||||
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
|
||||
} else {
|
||||
return loc.lnd.expired;
|
||||
}
|
||||
return loc.lnd.expired;
|
||||
}
|
||||
} else {
|
||||
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
|
||||
}
|
||||
}, [item, itemPriceUnit]);
|
||||
return formattedAmount;
|
||||
}, [item, formattedAmount]);
|
||||
|
||||
const rowTitleStyle = useMemo(() => {
|
||||
let color = colors.successColor;
|
||||
|
@ -198,10 +193,9 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
const { label: transactionTypeLabel, icon: avatar } = determineTransactionTypeAndAvatar();
|
||||
|
||||
const amountWithUnit = useMemo(() => {
|
||||
const amount = formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
|
||||
const unit = itemPriceUnit === BitcoinUnit.BTC || itemPriceUnit === BitcoinUnit.SATS ? ` ${itemPriceUnit}` : ' ';
|
||||
return `${amount}${unit}`;
|
||||
}, [item.value, itemPriceUnit]);
|
||||
const unitSuffix = itemPriceUnit === BitcoinUnit.BTC || itemPriceUnit === BitcoinUnit.SATS ? ` ${itemPriceUnit}` : ' ';
|
||||
return `${formattedAmount}${unitSuffix}`;
|
||||
}, [formattedAmount, itemPriceUnit]);
|
||||
|
||||
useEffect(() => {
|
||||
setSubtitleNumberOfLines(1);
|
||||
|
@ -226,7 +220,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
}
|
||||
const loaded = await LN.loadSuccessfulPayment(paymentHash);
|
||||
if (loaded) {
|
||||
navigate('ScanLndInvoiceRoot', {
|
||||
navigate('ScanLNDInvoiceRoot', {
|
||||
screen: 'LnurlPaySuccess',
|
||||
params: {
|
||||
paymentHash,
|
||||
|
@ -252,7 +246,19 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
setSubtitleNumberOfLines(0);
|
||||
}, []);
|
||||
|
||||
const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]);
|
||||
const handleOnDetailsPress = useCallback(() => {
|
||||
if (walletID && item && item.hash) {
|
||||
navigate('TransactionDetails', { tx: item, hash: item.hash, walletID });
|
||||
} else {
|
||||
const lightningWallet = wallets.find(wallet => wallet?.getID() === item.walletID);
|
||||
if (lightningWallet) {
|
||||
navigate('LNDViewInvoice', {
|
||||
invoice: item,
|
||||
walletID: lightningWallet.getID(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [item, navigate, walletID, wallets]);
|
||||
|
||||
const handleOnCopyAmountTap = useCallback(() => Clipboard.setString(rowTitle.replace(/[\s\\-]/g, '')), [rowTitle]);
|
||||
const handleOnCopyTransactionID = useCallback(() => Clipboard.setString(item.hash), [item.hash]);
|
||||
|
@ -283,6 +289,8 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
handleCopyOpenInBlockExplorerPress();
|
||||
} else if (id === CommonToolTipActions.CopyTXID.id) {
|
||||
handleOnCopyTransactionID();
|
||||
} else if (id === CommonToolTipActions.Details.id) {
|
||||
handleOnDetailsPress();
|
||||
}
|
||||
},
|
||||
[
|
||||
|
@ -290,31 +298,40 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
handleOnCopyAmountTap,
|
||||
handleOnCopyNote,
|
||||
handleOnCopyTransactionID,
|
||||
handleOnDetailsPress,
|
||||
handleOnExpandNote,
|
||||
handleOnViewOnBlockExplorer,
|
||||
],
|
||||
);
|
||||
const toolTipActions = useMemo((): Action[] => {
|
||||
const actions: (Action | Action[])[] = [];
|
||||
|
||||
if (rowTitle !== loc.lnd.expired) {
|
||||
actions.push(CommonToolTipActions.CopyAmount);
|
||||
}
|
||||
|
||||
if (subtitle) {
|
||||
actions.push(CommonToolTipActions.CopyNote);
|
||||
}
|
||||
|
||||
if (item.hash) {
|
||||
actions.push(CommonToolTipActions.CopyTXID, CommonToolTipActions.CopyBlockExplorerLink, [CommonToolTipActions.OpenInBlockExplorer]);
|
||||
}
|
||||
|
||||
if (subtitle && subtitleNumberOfLines === 1) {
|
||||
actions.push([CommonToolTipActions.ExpandNote]);
|
||||
}
|
||||
const actions: (Action | Action[])[] = [
|
||||
{
|
||||
...CommonToolTipActions.CopyAmount,
|
||||
hidden: rowTitle === loc.lnd.expired,
|
||||
},
|
||||
{
|
||||
...CommonToolTipActions.CopyNote,
|
||||
hidden: !subtitle,
|
||||
},
|
||||
{
|
||||
...CommonToolTipActions.CopyTXID,
|
||||
hidden: !item.hash,
|
||||
},
|
||||
{
|
||||
...CommonToolTipActions.CopyBlockExplorerLink,
|
||||
hidden: !item.hash,
|
||||
},
|
||||
[{ ...CommonToolTipActions.OpenInBlockExplorer, hidden: !item.hash }, CommonToolTipActions.Details],
|
||||
[
|
||||
{
|
||||
...CommonToolTipActions.ExpandNote,
|
||||
hidden: subtitleNumberOfLines !== 1,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
return actions as Action[];
|
||||
}, [item.hash, subtitle, rowTitle, subtitleNumberOfLines]);
|
||||
}, [rowTitle, subtitle, item.hash, subtitleNumberOfLines]);
|
||||
|
||||
const accessibilityState = useMemo(() => {
|
||||
return {
|
||||
|
@ -322,6 +339,8 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
};
|
||||
}, [subtitleNumberOfLines]);
|
||||
|
||||
const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]);
|
||||
|
||||
return (
|
||||
<ToolTipMenu
|
||||
isButton
|
||||
|
@ -343,8 +362,17 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
rightTitle={rowTitle}
|
||||
rightTitleStyle={rowTitleStyle}
|
||||
containerStyle={combinedStyle}
|
||||
testID="TransactionListItem"
|
||||
/>
|
||||
</ToolTipMenu>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.item.hash === nextProps.item.hash &&
|
||||
prevProps.item.received === nextProps.item.received &&
|
||||
prevProps.itemPriceUnit === nextProps.itemPriceUnit &&
|
||||
prevProps.walletID === nextProps.walletID
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -108,13 +108,14 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
|||
];
|
||||
}, []);
|
||||
|
||||
const balance = useMemo(() => {
|
||||
const balanceFormatted =
|
||||
unit === BitcoinUnit.LOCAL_CURRENCY
|
||||
? formatBalance(wallet.getBalance(), unit, true)
|
||||
: formatBalanceWithoutSuffix(wallet.getBalance(), unit, true);
|
||||
return !hideBalance && balanceFormatted;
|
||||
}, [unit, wallet, hideBalance]);
|
||||
const currentBalance = wallet ? wallet.getBalance() : 0;
|
||||
const formattedBalance = useMemo(() => {
|
||||
return unit === BitcoinUnit.LOCAL_CURRENCY
|
||||
? formatBalance(currentBalance, unit, true)
|
||||
: formatBalanceWithoutSuffix(currentBalance, unit, true);
|
||||
}, [unit, currentBalance]);
|
||||
|
||||
const balance = !wallet.hideBalance && formattedBalance;
|
||||
|
||||
const toolTipWalletBalanceActions = useMemo(() => {
|
||||
return hideBalance
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
FlatList,
|
||||
|
@ -98,8 +98,12 @@ interface WalletCarouselItemProps {
|
|||
isSelectedWallet?: boolean;
|
||||
customStyle?: ViewStyle;
|
||||
horizontal?: boolean;
|
||||
isPlaceHolder?: boolean;
|
||||
searchQuery?: string;
|
||||
renderHighlightedText?: (text: string, query: string) => JSX.Element;
|
||||
animationsEnabled?: boolean;
|
||||
onPressIn?: () => void;
|
||||
onPressOut?: () => void;
|
||||
}
|
||||
|
||||
const iStyles = StyleSheet.create({
|
||||
|
@ -161,21 +165,6 @@ const iStyles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
interface WalletCarouselItemProps {
|
||||
item: TWallet;
|
||||
onPress: (item: TWallet) => void;
|
||||
handleLongPress?: () => void;
|
||||
isSelectedWallet?: boolean;
|
||||
customStyle?: ViewStyle;
|
||||
horizontal?: boolean;
|
||||
isPlaceHolder?: boolean;
|
||||
searchQuery?: string;
|
||||
renderHighlightedText?: (text: string, query: string) => JSX.Element;
|
||||
animationsEnabled?: boolean;
|
||||
onPressIn?: () => void;
|
||||
onPressOut?: () => void;
|
||||
}
|
||||
|
||||
export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
||||
({
|
||||
item,
|
||||
|
@ -198,34 +187,31 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
|||
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
|
||||
const { isLargeScreen } = useIsLargeScreen();
|
||||
|
||||
const springConfig = useMemo(() => ({ useNativeDriver: true, tension: 100 }), []);
|
||||
const animateScale = useCallback(
|
||||
(toValue: number, callback?: () => void) => {
|
||||
Animated.spring(scaleValue, { toValue, ...springConfig }).start(callback);
|
||||
},
|
||||
[scaleValue, springConfig],
|
||||
);
|
||||
|
||||
const onPressedIn = useCallback(() => {
|
||||
if (animationsEnabled) {
|
||||
Animated.spring(scaleValue, {
|
||||
toValue: 0.95,
|
||||
useNativeDriver: true,
|
||||
friction: 3,
|
||||
tension: 100,
|
||||
}).start();
|
||||
animateScale(0.95);
|
||||
}
|
||||
if (onPressIn) onPressIn();
|
||||
}, [scaleValue, animationsEnabled, onPressIn]);
|
||||
}, [animateScale, animationsEnabled, onPressIn]);
|
||||
|
||||
const onPressedOut = useCallback(() => {
|
||||
if (animationsEnabled) {
|
||||
Animated.spring(scaleValue, {
|
||||
toValue: 1.0,
|
||||
useNativeDriver: true,
|
||||
friction: 3,
|
||||
tension: 100,
|
||||
}).start();
|
||||
animateScale(1.0);
|
||||
}
|
||||
if (onPressOut) onPressOut();
|
||||
}, [scaleValue, animationsEnabled, onPressOut]);
|
||||
}, [animateScale, animationsEnabled, onPressOut]);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
onPressedOut();
|
||||
onPress(item);
|
||||
}, [item, onPress, onPressedOut]);
|
||||
}, [item, onPress]);
|
||||
|
||||
const opacity = isSelectedWallet === false ? 0.5 : 1.0;
|
||||
let image;
|
||||
|
@ -267,6 +253,8 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
|||
if (handleLongPress) handleLongPress();
|
||||
}}
|
||||
onPress={handlePress}
|
||||
delayHoverIn={0}
|
||||
delayHoverOut={0}
|
||||
>
|
||||
<View style={[iStyles.shadowContainer, { backgroundColor: colors.background, shadowColor: colors.shadowColor }]}>
|
||||
<LinearGradient colors={WalletGradient.gradientsFor(item.type)} style={iStyles.grad}>
|
||||
|
@ -362,6 +350,10 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
|||
renderHighlightedText,
|
||||
isFlatList = true,
|
||||
} = props;
|
||||
|
||||
const { width } = useWindowDimensions();
|
||||
const itemWidth = React.useMemo(() => (width * 0.82 > 375 ? 375 : width * 0.82), [width]);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item, index }: ListRenderItemInfo<TWallet>) =>
|
||||
item ? (
|
||||
|
@ -379,7 +371,6 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
|||
);
|
||||
|
||||
const flatListRef = useRef<FlatList<any>>(null);
|
||||
|
||||
useImperativeHandle(ref, (): any => {
|
||||
return {
|
||||
scrollToEnd: (params: { animated?: boolean | null | undefined } | undefined) => flatListRef.current?.scrollToEnd(params),
|
||||
|
@ -401,10 +392,8 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
|||
getNativeScrollRef: () => flatListRef.current?.getNativeScrollRef(),
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onScrollToIndexFailed = (error: { averageItemLength: number; index: number }): void => {
|
||||
console.debug('onScrollToIndexFailed');
|
||||
console.debug(error);
|
||||
console.debug('onScrollToIndexFailed', error);
|
||||
flatListRef.current?.scrollToOffset({ offset: error.averageItemLength * error.index, animated: true });
|
||||
setTimeout(() => {
|
||||
if (data.length !== 0 && flatListRef.current !== null) {
|
||||
|
@ -413,16 +402,16 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
|||
}, 100);
|
||||
};
|
||||
|
||||
const { width } = useWindowDimensions();
|
||||
const sliderHeight = 195;
|
||||
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
|
||||
|
||||
const keyExtractor = useCallback((item: TWallet, index: number) => (item?.getID ? item.getID() : index.toString()), []);
|
||||
|
||||
return isFlatList ? (
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
renderItem={renderItem}
|
||||
extraData={data}
|
||||
keyExtractor={(_, index) => index.toString()}
|
||||
keyExtractor={keyExtractor}
|
||||
showsVerticalScrollIndicator={false}
|
||||
pagingEnabled={horizontal}
|
||||
disableIntervalMomentum={horizontal}
|
||||
|
@ -433,6 +422,7 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
|||
showsHorizontalScrollIndicator={false}
|
||||
initialNumToRender={10}
|
||||
scrollEnabled={scrollEnabled}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
style={{ minHeight: sliderHeight + 12 }}
|
||||
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||
|
|
|
@ -40,9 +40,7 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
|
|||
borderBottomColor: colors.lightBorder,
|
||||
backgroundColor: colors.elevated,
|
||||
},
|
||||
list: {
|
||||
color: colors.buttonTextColor,
|
||||
},
|
||||
|
||||
index: {
|
||||
color: colors.alternativeTextColor,
|
||||
},
|
||||
|
@ -151,24 +149,29 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
|
|||
title={item.address}
|
||||
actions={menuActions}
|
||||
onPressMenuItem={onToolTipPress}
|
||||
// Revisit once RNMenu has renderPreview prop
|
||||
renderPreview={renderPreview}
|
||||
onPress={navigateToReceive}
|
||||
isButton
|
||||
>
|
||||
<ListItem key={item.key} containerStyle={stylesHook.container}>
|
||||
<ListItem.Content style={stylesHook.list}>
|
||||
<ListItem.Title style={stylesHook.list} numberOfLines={1} ellipsizeMode="middle">
|
||||
<Text style={[styles.index, stylesHook.index]}>{item.index + 1}</Text>{' '}
|
||||
<Text style={[stylesHook.address, styles.address]}>{item.address}</Text>
|
||||
</ListItem.Title>
|
||||
<View style={styles.subtitle}>
|
||||
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>{balance}</Text>
|
||||
<ListItem.Content>
|
||||
<View style={styles.row}>
|
||||
<View style={styles.leftSection}>
|
||||
<Text style={[styles.index, stylesHook.index]}>{item.index}</Text>
|
||||
</View>
|
||||
<View style={styles.middleSection}>
|
||||
<Text style={[stylesHook.address, styles.address]} numberOfLines={1} ellipsizeMode="middle">
|
||||
{item.address}
|
||||
</Text>
|
||||
<Text style={[stylesHook.balance, styles.balance]}>{balance}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ListItem.Content>
|
||||
<View>
|
||||
<View style={styles.rightContainer}>
|
||||
<AddressTypeBadge isInternal={item.isInternal} hasTransactions={hasTransactions} />
|
||||
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>
|
||||
{loc.addresses.transactions}: {item.transactions}
|
||||
<Text style={[stylesHook.balance, styles.balance]}>
|
||||
{loc.addresses.transactions}: {item.transactions ?? 0}
|
||||
</Text>
|
||||
</View>
|
||||
</ListItem>
|
||||
|
@ -179,20 +182,27 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
|
|||
const styles = StyleSheet.create({
|
||||
address: {
|
||||
fontWeight: 'bold',
|
||||
marginHorizontal: 40,
|
||||
marginHorizontal: 4,
|
||||
},
|
||||
index: {
|
||||
fontSize: 15,
|
||||
},
|
||||
balance: {
|
||||
marginTop: 8,
|
||||
marginLeft: 14,
|
||||
marginTop: 4,
|
||||
},
|
||||
subtitle: {
|
||||
flex: 1,
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
leftSection: {
|
||||
marginRight: 8,
|
||||
},
|
||||
middleSection: {
|
||||
flex: 1,
|
||||
},
|
||||
rightContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { NativeStackNavigationOptions } from '@react-navigation/native-stack';
|
||||
import React from 'react';
|
||||
import { Image, Keyboard, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import { Image, Keyboard, Platform, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
|
||||
import loc from '../loc';
|
||||
import { Theme } from './themes';
|
||||
|
@ -59,7 +59,6 @@ const navigationStyle = (
|
|||
{
|
||||
closeButtonPosition,
|
||||
onCloseButtonPressed,
|
||||
headerBackVisible = true,
|
||||
...opts
|
||||
}: NativeStackNavigationOptions & {
|
||||
closeButtonPosition?: CloseButtonPosition;
|
||||
|
@ -78,11 +77,6 @@ const navigationStyle = (
|
|||
let headerRight;
|
||||
let headerLeft;
|
||||
|
||||
if (!headerBackVisible) {
|
||||
headerLeft = () => <></>;
|
||||
opts.headerLeft = headerLeft;
|
||||
}
|
||||
|
||||
if (closeButton === CloseButtonPosition.Right) {
|
||||
headerRight = () => (
|
||||
<TouchableOpacity
|
||||
|
@ -108,17 +102,24 @@ const navigationStyle = (
|
|||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
let options: NativeStackNavigationOptions = {
|
||||
const baseHeaderStyle = {
|
||||
headerShadowVisible: false,
|
||||
headerTitleStyle: {
|
||||
fontWeight: '600',
|
||||
fontWeight: '600' as const,
|
||||
color: theme.colors.foregroundColor,
|
||||
},
|
||||
headerBackTitleVisible: false,
|
||||
headerTintColor: theme.colors.foregroundColor,
|
||||
headerBackButtonDisplayMode: 'minimal',
|
||||
};
|
||||
const isLeftCloseButtonAndroid = closeButton === CloseButtonPosition.Left && Platform.OS === 'android';
|
||||
|
||||
const leftCloseButtonStyle = isLeftCloseButtonAndroid ? { headerBackImageSource: theme.closeImage } : { headerLeft };
|
||||
|
||||
let options: NativeStackNavigationOptions = {
|
||||
...baseHeaderStyle,
|
||||
...leftCloseButtonStyle,
|
||||
headerBackButtonDisplayMode: 'minimal',
|
||||
headerRight,
|
||||
headerLeft,
|
||||
...opts,
|
||||
};
|
||||
|
||||
|
|
|
@ -128,18 +128,18 @@ end
|
|||
|
||||
You can test it on the following devices:
|
||||
|
||||
- [Google Pixel 9 (Android 15)](https://app-live.browserstack.com/dashboard#os=android&os_version=15.0&device=Google+Pixel+8&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 8 (Android 14)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Google+Pixel+8&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 7 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Google+Pixel+7&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 5 (Android 12)](https://app-live.browserstack.com/dashboard#os=android&os_version=12.0&device=Google+Pixel+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 3a (Android 9)](https://app-live.browserstack.com/dashboard#os=android&os_version=9.0&device=Google+Pixel+3a&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 9 (Android 15)](https://app-live.browserstack.com/dashboard#os=android&os_version=15.0&device=Google+Pixel+8&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
- [Google Pixel 8 (Android 14)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Google+Pixel+8&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
- [Google Pixel 7 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Google+Pixel+7&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
- [Google Pixel 5 (Android 12)](https://app-live.browserstack.com/dashboard#os=android&os_version=12.0&device=Google+Pixel+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
- [Google Pixel 3a (Android 9)](https://app-live.browserstack.com/dashboard#os=android&os_version=9.0&device=Google+Pixel+3a&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
|
||||
- [Samsung Galaxy Z Fold 6 (Android 14)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Samsung+Galaxy+Z+Fold+6&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Samsung Galaxy Z Fold 5 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Z+Fold+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Samsung Galaxy Tab S9 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Tab+S9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Samsung Galaxy Note 9 (Android 8.1)](https://app-live.browserstack.com/dashboard#os=android&os_version=8.1&device=Samsung+Galaxy+Note+9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Samsung Galaxy Z Fold 6 (Android 14)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Samsung+Galaxy+Z+Fold+6&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
- [Samsung Galaxy Z Fold 5 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Z+Fold+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
- [Samsung Galaxy Tab S9 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Tab+S9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
- [Samsung Galaxy Note 9 (Android 8.1)](https://app-live.browserstack.com/dashboard#os=android&os_version=8.1&device=Samsung+Galaxy+Note+9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
|
||||
- [OnePlus 11R (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=OnePlus+11R&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [OnePlus 11R (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=OnePlus+11R&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
**Filename**: [#{apk_filename}](#{apk_download_url})
|
||||
**BrowserStack App URL**: #{app_url}
|
||||
COMMENT
|
||||
|
@ -192,6 +192,40 @@ end
|
|||
# ===========================
|
||||
|
||||
platform :ios do
|
||||
# Add helper methods for error handling and retries
|
||||
def ensure_env_vars(vars)
|
||||
vars.each do |var|
|
||||
UI.user_error!("#{var} environment variable is missing") if ENV[var].nil? || ENV[var].empty?
|
||||
end
|
||||
end
|
||||
|
||||
def log_success(message)
|
||||
UI.success("✅ #{message}")
|
||||
end
|
||||
|
||||
def log_error(message)
|
||||
UI.error("❌ #{message}")
|
||||
end
|
||||
|
||||
# Method to safely call actions with retry logic
|
||||
def with_retry(max_attempts = 3, action_name = "")
|
||||
attempts = 0
|
||||
begin
|
||||
attempts += 1
|
||||
yield
|
||||
rescue => e
|
||||
if attempts < max_attempts
|
||||
wait_time = 10 * attempts
|
||||
log_error("Attempt #{attempts}/#{max_attempts} for #{action_name} failed: #{e.message}")
|
||||
UI.message("Retrying in #{wait_time} seconds...")
|
||||
sleep(wait_time)
|
||||
retry
|
||||
else
|
||||
log_error("#{action_name} failed after #{max_attempts} attempts: #{e.message}")
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Register new devices from a file"
|
||||
lane :register_devices_from_txt do
|
||||
|
@ -238,26 +272,33 @@ platform :ios do
|
|||
|
||||
desc "Synchronize certificates and provisioning profiles"
|
||||
lane :setup_provisioning_profiles do
|
||||
required_vars = ["GIT_ACCESS_TOKEN", "GIT_URL", "ITC_TEAM_ID", "ITC_TEAM_NAME", "KEYCHAIN_PASSWORD"]
|
||||
ensure_env_vars(required_vars)
|
||||
|
||||
UI.message("Setting up provisioning profiles...")
|
||||
|
||||
platform = "ios"
|
||||
|
||||
|
||||
# Iterate over app identifiers to fetch provisioning profiles
|
||||
app_identifiers.each do |app_identifier|
|
||||
match(
|
||||
git_basic_authorization: ENV["GIT_ACCESS_TOKEN"],
|
||||
git_url: ENV["GIT_URL"],
|
||||
type: "appstore",
|
||||
clone_branch_directly: true, # Skip if the branch already exists
|
||||
platform: platform,
|
||||
app_identifier: app_identifier,
|
||||
team_id: ENV["ITC_TEAM_ID"],
|
||||
team_name: ENV["ITC_TEAM_NAME"],
|
||||
readonly: true,
|
||||
keychain_name: "temp_keychain",
|
||||
keychain_password: ENV["KEYCHAIN_PASSWORD"]
|
||||
)
|
||||
with_retry(3, "Fetching provisioning profile for #{app_identifier}") do
|
||||
UI.message("Fetching provisioning profile for #{app_identifier}...")
|
||||
match(
|
||||
git_basic_authorization: ENV["GIT_ACCESS_TOKEN"],
|
||||
git_url: ENV["GIT_URL"],
|
||||
type: "appstore",
|
||||
clone_branch_directly: true,
|
||||
platform: "ios",
|
||||
app_identifier: app_identifier,
|
||||
team_id: ENV["ITC_TEAM_ID"],
|
||||
team_name: ENV["ITC_TEAM_NAME"],
|
||||
readonly: true,
|
||||
keychain_name: "temp_keychain",
|
||||
keychain_password: ENV["KEYCHAIN_PASSWORD"]
|
||||
)
|
||||
log_success("Successfully fetched provisioning profile for #{app_identifier}")
|
||||
end
|
||||
end
|
||||
|
||||
log_success("All provisioning profiles set up")
|
||||
end
|
||||
|
||||
desc "Fetch development certificates and provisioning profiles for Mac Catalyst"
|
||||
|
@ -402,48 +443,114 @@ lane :upload_bugsnag_sourcemaps do
|
|||
end
|
||||
|
||||
desc "Build the iOS app"
|
||||
lane :build_app_lane do
|
||||
Dir.chdir(project_root) do
|
||||
UI.message("Building the application from: #{Dir.pwd}")
|
||||
lane :build_app_lane do
|
||||
Dir.chdir(project_root) do
|
||||
UI.message("Building the application from: #{Dir.pwd}")
|
||||
|
||||
workspace_path = File.join(project_root, "ios", "BlueWallet.xcworkspace")
|
||||
export_options_path = File.join(project_root, "ios", "export_options.plist")
|
||||
workspace_path = File.join(project_root, "ios", "BlueWallet.xcworkspace")
|
||||
export_options_path = File.join(project_root, "ios", "export_options.plist")
|
||||
|
||||
clear_derived_data_lane
|
||||
|
||||
begin
|
||||
build_ios_app(
|
||||
scheme: "BlueWallet",
|
||||
workspace: workspace_path,
|
||||
export_method: "app-store",
|
||||
include_bitcode: false,
|
||||
configuration: "Release",
|
||||
skip_profile_detection: false,
|
||||
include_symbols: true,
|
||||
export_team_id: ENV["ITC_TEAM_ID"],
|
||||
export_options: export_options_path,
|
||||
output_directory: File.join(project_root, "ios", "build"),
|
||||
output_name: "BlueWallet_#{ENV['PROJECT_VERSION']}_#{ENV['NEW_BUILD_NUMBER']}.ipa",
|
||||
buildlog_path: File.join(project_root, "ios", "build_logs"),
|
||||
silent: false,
|
||||
clean: true
|
||||
)
|
||||
rescue => e
|
||||
UI.user_error!("build_ios_app failed: #{e.message}")
|
||||
end
|
||||
clear_derived_data_lane
|
||||
|
||||
# Determine which iOS version to use
|
||||
ios_version = determine_ios_version
|
||||
|
||||
# Use File.join to construct paths without extra slashes
|
||||
ipa_path = lane_context[SharedValues::IPA_OUTPUT_PATH]
|
||||
UI.message("Using iOS version: #{ios_version}")
|
||||
UI.message("Using export options from: #{export_options_path}")
|
||||
|
||||
# Define the IPA output path before building
|
||||
ipa_directory = File.join(project_root, "ios", "build")
|
||||
ipa_name = "BlueWallet_#{ENV['PROJECT_VERSION']}_#{ENV['NEW_BUILD_NUMBER']}.ipa"
|
||||
ipa_path = File.join(ipa_directory, ipa_name)
|
||||
|
||||
begin
|
||||
build_ios_app(
|
||||
scheme: "BlueWallet",
|
||||
workspace: workspace_path,
|
||||
export_method: "app-store",
|
||||
export_options: export_options_path,
|
||||
output_directory: ipa_directory,
|
||||
output_name: ipa_name,
|
||||
buildlog_path: File.join(project_root, "ios", "build_logs"),
|
||||
)
|
||||
rescue => e
|
||||
UI.user_error!("build_ios_app failed: #{e.message}")
|
||||
end
|
||||
|
||||
if ipa_path && File.exist?(ipa_path)
|
||||
UI.message("IPA successfully found at: #{ipa_path}")
|
||||
# Check for IPA path from both our defined path and fastlane's context
|
||||
ipa_path = lane_context[SharedValues::IPA_OUTPUT_PATH] || ipa_path
|
||||
|
||||
# Ensure the directory exists
|
||||
FileUtils.mkdir_p(File.dirname(ipa_path)) unless Dir.exist?(File.dirname(ipa_path))
|
||||
|
||||
if ipa_path && File.exist?(ipa_path)
|
||||
UI.message("IPA successfully found at: #{ipa_path}")
|
||||
else
|
||||
# Try to find any IPA file as fallback
|
||||
Dir.chdir(project_root) do
|
||||
fallback_ipa = Dir.glob("**/*.ipa").first
|
||||
if fallback_ipa
|
||||
ipa_path = File.join(project_root, fallback_ipa)
|
||||
UI.message("Found fallback IPA at: #{ipa_path}")
|
||||
else
|
||||
UI.user_error!("No IPA file found after build")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Set both environment variable and GitHub Actions output
|
||||
ENV['IPA_OUTPUT_PATH'] = ipa_path
|
||||
sh("echo 'IPA_OUTPUT_PATH=#{ipa_path}' >> $GITHUB_ENV") # Export for GitHub Actions
|
||||
else
|
||||
UI.user_error!("IPA not found after build_ios_app.")
|
||||
# Set both standard output format and the newer GITHUB_OUTPUT format
|
||||
sh("echo 'ipa_output_path=#{ipa_path}' >> $GITHUB_OUTPUT") if ENV['GITHUB_OUTPUT']
|
||||
sh("echo ::set-output name=ipa_output_path::#{ipa_path}")
|
||||
|
||||
# Also write path to a file that can be read by subsequent steps
|
||||
ipa_path_file = "#{ipa_directory}/ipa_path.txt"
|
||||
File.write(ipa_path_file, ipa_path)
|
||||
UI.success("Saved IPA path to: #{ipa_path_file}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Delete temporary keychain"
|
||||
lane :delete_temp_keychain do
|
||||
UI.message("Deleting temporary keychain...")
|
||||
|
||||
delete_keychain(
|
||||
name: "temp_keychain"
|
||||
) if File.exist?(File.expand_path("~/Library/Keychains/temp_keychain-db"))
|
||||
|
||||
UI.message("Temporary keychain deleted successfully.")
|
||||
end
|
||||
|
||||
# Helper method to determine which iOS version to use
|
||||
private_lane :determine_ios_version do
|
||||
# Get available iOS simulator runtimes
|
||||
runtimes_output = sh("xcrun simctl list runtimes 2>&1", log: false) rescue ""
|
||||
|
||||
if runtimes_output.include?("iOS")
|
||||
# Extract available iOS versions
|
||||
ios_versions = runtimes_output.scan(/iOS ([0-9.]+)/)
|
||||
.flatten
|
||||
.map { |v| Gem::Version.new(v) }
|
||||
.sort
|
||||
.reverse
|
||||
|
||||
if ios_versions.any?
|
||||
latest_version = ios_versions.first.to_s
|
||||
UI.success("Found iOS simulator version: #{latest_version}")
|
||||
latest_version # Implicit return - last expression is returned
|
||||
else
|
||||
# Default to a reasonable iOS version if none found
|
||||
UI.important("No iOS simulator versions found. Using default version.")
|
||||
"17.6" # Implicit return
|
||||
end
|
||||
else
|
||||
# Default to a reasonable iOS version if no iOS runtimes
|
||||
UI.important("No iOS simulator runtimes found. Using default version.")
|
||||
"17.6" # Implicit return
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
# ===========================
|
||||
# Global Lanes
|
||||
|
|
|
@ -3,33 +3,39 @@
|
|||
# URL of the Git repository to store the certificates
|
||||
git_url(ENV["GIT_URL"])
|
||||
|
||||
# Define the type of match to run, could be one of 'appstore', 'adhoc', 'development', or 'enterprise'.
|
||||
# For example, use 'appstore' for App Store builds, 'adhoc' for Ad Hoc distribution,
|
||||
# 'development' for development builds, and 'enterprise' for In-House (enterprise) distribution.
|
||||
type("appstore")
|
||||
# Define the type of match to run
|
||||
# Default to "appstore" but can be overridden
|
||||
type(ENV["MATCH_TYPE"] || "appstore")
|
||||
|
||||
app_identifier(["io.bluewallet.bluewallet", "io.bluewallet.bluewallet.watch", "io.bluewallet.bluewallet.watch.extension", "io.bluewallet.bluewallet.Stickers", "io.bluewallet.bluewallet.MarketWidget"]) # Replace with your app identifiers
|
||||
# App identifiers for all BlueWallet apps
|
||||
app_identifier([
|
||||
"io.bluewallet.bluewallet",
|
||||
"io.bluewallet.bluewallet.watch",
|
||||
"io.bluewallet.bluewallet.watch.extension",
|
||||
"io.bluewallet.bluewallet.Stickers",
|
||||
"io.bluewallet.bluewallet.MarketWidget"
|
||||
])
|
||||
|
||||
# List of app identifiers to create provisioning profiles for.
|
||||
# Replace with your app's bundle identifier(s).
|
||||
|
||||
# Your Apple Developer account email address.
|
||||
# Your Apple Developer account email address
|
||||
username(ENV["APPLE_ID"])
|
||||
|
||||
# The ID of your Apple Developer team if you're part of multiple teams
|
||||
# The ID of your Apple Developer team
|
||||
team_id(ENV["ITC_TEAM_ID"])
|
||||
|
||||
# Set this to true if match should only read existing certificates and profiles
|
||||
# and not create new ones.
|
||||
readonly(true)
|
||||
# Set readonly based on environment (default to true for safety)
|
||||
# Set to false explicitly when new profiles need to be created
|
||||
readonly(ENV["MATCH_READONLY"] == "false" ? false : true)
|
||||
|
||||
# Optional: The Git branch that is used for match.
|
||||
# Default is 'master'.
|
||||
|
||||
# Optional: Path to a specific SSH key to be used by match.
|
||||
# Only needed if you're using a private repository and match needs to use SSH keys for authentication.
|
||||
# ssh_key("/path/to/your/private/key")
|
||||
|
||||
# Optional: Define the platform to use, can be 'ios', 'macos', or 'tvos'.
|
||||
# For React Native projects, you'll typically use 'ios'.
|
||||
# Define the platform to use
|
||||
platform("ios")
|
||||
|
||||
# Git basic authentication through access token
|
||||
# This is useful for CI/CD environments where SSH keys aren't available
|
||||
git_basic_authorization(ENV["GIT_ACCESS_TOKEN"])
|
||||
|
||||
# Storage mode (git by default)
|
||||
storage_mode("git")
|
||||
|
||||
# Optional: The Git branch that is used for match
|
||||
# Default is 'master'
|
||||
# branch("main")
|
||||
|
|
|
@ -4,3 +4,4 @@
|
|||
|
||||
gem 'fastlane-plugin-browserstack'
|
||||
gem 'fastlane-plugin-bugsnag_sourcemaps_upload'
|
||||
gem "fastlane-plugin-bugsnag"
|
||||
|
|
1
gesture-handler.js
Normal file
1
gesture-handler.js
Normal file
|
@ -0,0 +1 @@
|
|||
// Don't import react-native-gesture-handler on web
|
2
gesture-handler.native.js
Normal file
2
gesture-handler.native.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
// Only import react-native-gesture-handler on native platforms
|
||||
import 'react-native-gesture-handler';
|
|
@ -1,5 +1,6 @@
|
|||
import { Platform } from 'react-native';
|
||||
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
|
||||
import { navigationRef } from '../NavigationService.ts';
|
||||
|
||||
const isCameraAuthorizationStatusGranted = async () => {
|
||||
const status = await check(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA);
|
||||
|
@ -10,4 +11,17 @@ const requestCameraAuthorization = () => {
|
|||
return request(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA);
|
||||
};
|
||||
|
||||
export { isCameraAuthorizationStatusGranted, requestCameraAuthorization };
|
||||
const scanQrHelper = async (): Promise<string> => {
|
||||
await requestCameraAuthorization();
|
||||
return new Promise(resolve => {
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.current?.navigate('ScanQRCode', {
|
||||
onBarScanned: (data: string) => {
|
||||
resolve(data);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export { isCameraAuthorizationStatusGranted, requestCameraAuthorization, scanQrHelper };
|
||||
|
|
23
helpers/screenProtect.ts
Normal file
23
helpers/screenProtect.ts
Normal 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
|
|
@ -1,5 +1,3 @@
|
|||
import 'react-native-gesture-handler'; // should be on top
|
||||
|
||||
import { CommonActions } from '@react-navigation/native';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { AppState, AppStateStatus, Linking } from 'react-native';
|
||||
|
@ -21,25 +19,42 @@ import loc from '../loc';
|
|||
import { Chain } from '../models/bitcoinUnits';
|
||||
import { navigationRef } from '../NavigationService';
|
||||
import ActionSheet from '../screen/ActionSheet';
|
||||
import { useStorage } from '../hooks/context/useStorage';
|
||||
import { useStorage } from './context/useStorage';
|
||||
import RNQRGenerator from 'rn-qr-generator';
|
||||
import presentAlert from './Alert';
|
||||
import useMenuElements from '../hooks/useMenuElements';
|
||||
import useWidgetCommunication from '../hooks/useWidgetCommunication';
|
||||
import useWatchConnectivity from '../hooks/useWatchConnectivity';
|
||||
import useDeviceQuickActions from '../hooks/useDeviceQuickActions';
|
||||
import useHandoffListener from '../hooks/useHandoffListener';
|
||||
import presentAlert from '../components/Alert';
|
||||
import useWidgetCommunication from './useWidgetCommunication';
|
||||
import useWatchConnectivity from './useWatchConnectivity';
|
||||
import useDeviceQuickActions from './useDeviceQuickActions';
|
||||
import useHandoffListener from './useHandoffListener';
|
||||
import useMenuElements from './useMenuElements';
|
||||
|
||||
const ClipboardContentType = Object.freeze({
|
||||
BITCOIN: 'BITCOIN',
|
||||
LIGHTNING: 'LIGHTNING',
|
||||
});
|
||||
|
||||
const CompanionDelegates = () => {
|
||||
const { wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions, setSharedCosigner } = useStorage();
|
||||
/**
|
||||
* Hook that initializes all companion listeners and functionality without rendering a component
|
||||
*/
|
||||
const useCompanionListeners = (skipIfNotInitialized = true) => {
|
||||
const {
|
||||
wallets,
|
||||
addWallet,
|
||||
saveToDisk,
|
||||
fetchAndSaveWalletTransactions,
|
||||
refreshAllWalletTransactions,
|
||||
setSharedCosigner,
|
||||
walletsInitialized,
|
||||
} = useStorage();
|
||||
const appState = useRef<AppStateStatus>(AppState.currentState);
|
||||
const clipboardContent = useRef<undefined | string>();
|
||||
|
||||
// We need to call hooks unconditionally before any conditional logic
|
||||
// We'll use this check inside the effects to conditionally run logic
|
||||
const shouldActivateListeners = !skipIfNotInitialized || walletsInitialized;
|
||||
|
||||
// Initialize other hooks regardless of activation status
|
||||
// They'll handle their own conditional logic internally
|
||||
useWatchConnectivity();
|
||||
useWidgetCommunication();
|
||||
useMenuElements();
|
||||
|
@ -47,6 +62,8 @@ const CompanionDelegates = () => {
|
|||
useHandoffListener();
|
||||
|
||||
const processPushNotifications = useCallback(async () => {
|
||||
if (!shouldActivateListeners) return false;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
try {
|
||||
const notifications2process = await getStoredNotifications();
|
||||
|
@ -166,49 +183,58 @@ const CompanionDelegates = () => {
|
|||
console.error('Failed to process push notifications:', error);
|
||||
}
|
||||
return false;
|
||||
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets]);
|
||||
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets, shouldActivateListeners]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldActivateListeners) return;
|
||||
|
||||
initializeNotifications(processPushNotifications);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [shouldActivateListeners]);
|
||||
|
||||
const handleOpenURL = useCallback(
|
||||
async (event: { url: string }): Promise<void> => {
|
||||
const { url } = event;
|
||||
if (!shouldActivateListeners) return;
|
||||
|
||||
if (url) {
|
||||
const decodedUrl = decodeURIComponent(url);
|
||||
const fileName = decodedUrl.split('/').pop()?.toLowerCase();
|
||||
|
||||
if (fileName && /\.(jpe?g|png)$/i.test(fileName)) {
|
||||
try {
|
||||
if (!event.url) return;
|
||||
let decodedUrl: string;
|
||||
try {
|
||||
decodedUrl = decodeURIComponent(event.url);
|
||||
} catch (e) {
|
||||
console.error('Failed to decode URL, using original', e);
|
||||
decodedUrl = event.url;
|
||||
}
|
||||
const fileName = decodedUrl.split('/').pop()?.toLowerCase() || '';
|
||||
if (/\.(jpe?g|png)$/i.test(fileName)) {
|
||||
let qrResult;
|
||||
try {
|
||||
if (!decodedUrl) {
|
||||
throw new Error(loc.send.qr_error_no_qrcode);
|
||||
qrResult = await RNQRGenerator.detect({ uri: decodedUrl });
|
||||
} catch (e) {
|
||||
console.error('QR detection first attempt failed:', e);
|
||||
}
|
||||
if (!qrResult || !qrResult.values || qrResult.values.length === 0) {
|
||||
const altUrl = decodedUrl.replace(/^file:\/\//, '');
|
||||
try {
|
||||
qrResult = await RNQRGenerator.detect({ uri: altUrl });
|
||||
} catch (e) {
|
||||
console.error('QR detection second attempt failed:', e);
|
||||
}
|
||||
const values = await RNQRGenerator.detect({
|
||||
uri: decodedUrl,
|
||||
});
|
||||
|
||||
if (values && values.values.length > 0) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
DeeplinkSchemaMatch.navigationRouteFor(
|
||||
{ url: values.values[0] },
|
||||
(value: [string, any]) => navigationRef.navigate(...value),
|
||||
{
|
||||
wallets,
|
||||
addWallet,
|
||||
saveToDisk,
|
||||
setSharedCosigner,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
throw new Error(loc.send.qr_error_no_qrcode);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error detecting QR code:', error);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
presentAlert({ message: loc.send.qr_error_no_qrcode });
|
||||
}
|
||||
if (qrResult?.values?.length) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
DeeplinkSchemaMatch.navigationRouteFor(
|
||||
{ url: qrResult.values[0] },
|
||||
(value: [string, any]) => navigationRef.navigate(...value),
|
||||
{
|
||||
wallets,
|
||||
addWallet,
|
||||
saveToDisk,
|
||||
setSharedCosigner,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
throw new Error(loc.send.qr_error_no_qrcode);
|
||||
}
|
||||
} else {
|
||||
DeeplinkSchemaMatch.navigationRouteFor(event, (value: [string, any]) => navigationRef.navigate(...value), {
|
||||
|
@ -218,12 +244,19 @@ const CompanionDelegates = () => {
|
|||
setSharedCosigner,
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error in handleOpenURL:', err);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
presentAlert({ message: err.message || loc.send.qr_error_no_qrcode });
|
||||
}
|
||||
},
|
||||
[wallets, addWallet, saveToDisk, setSharedCosigner],
|
||||
[wallets, addWallet, saveToDisk, setSharedCosigner, shouldActivateListeners],
|
||||
);
|
||||
|
||||
const showClipboardAlert = useCallback(
|
||||
({ contentType }: { contentType: undefined | string }) => {
|
||||
if (!shouldActivateListeners) return;
|
||||
|
||||
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
|
||||
getClipboardContent().then(clipboard => {
|
||||
if (!clipboard) return;
|
||||
|
@ -246,12 +279,13 @@ const CompanionDelegates = () => {
|
|||
);
|
||||
});
|
||||
},
|
||||
[handleOpenURL],
|
||||
[handleOpenURL, shouldActivateListeners],
|
||||
);
|
||||
|
||||
const handleAppStateChange = useCallback(
|
||||
async (nextAppState: AppStateStatus | undefined) => {
|
||||
if (wallets.length === 0) return;
|
||||
if (!shouldActivateListeners || wallets.length === 0) return;
|
||||
|
||||
if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) {
|
||||
setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000);
|
||||
updateExchangeRate();
|
||||
|
@ -291,10 +325,12 @@ const CompanionDelegates = () => {
|
|||
appState.current = nextAppState;
|
||||
}
|
||||
},
|
||||
[processPushNotifications, showClipboardAlert, wallets],
|
||||
[processPushNotifications, showClipboardAlert, wallets, shouldActivateListeners],
|
||||
);
|
||||
|
||||
const addListeners = useCallback(() => {
|
||||
if (!shouldActivateListeners) return { urlSubscription: null, appStateSubscription: null };
|
||||
|
||||
const urlSubscription = Linking.addEventListener('url', handleOpenURL);
|
||||
const appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
|
||||
|
||||
|
@ -302,18 +338,16 @@ const CompanionDelegates = () => {
|
|||
urlSubscription,
|
||||
appStateSubscription,
|
||||
};
|
||||
}, [handleOpenURL, handleAppStateChange]);
|
||||
}, [handleOpenURL, handleAppStateChange, shouldActivateListeners]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscriptions = addListeners();
|
||||
|
||||
return () => {
|
||||
subscriptions.urlSubscription?.remove();
|
||||
subscriptions.appStateSubscription?.remove();
|
||||
subscriptions.urlSubscription?.remove?.();
|
||||
subscriptions.appStateSubscription?.remove?.();
|
||||
};
|
||||
}, [addListeners]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default CompanionDelegates;
|
||||
export default useCompanionListeners;
|
|
@ -1,23 +1,27 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import debounce from '../blue_modules/debounce';
|
||||
|
||||
const useDebounce = <T>(value: T, delay: number): T => {
|
||||
// Overload signatures
|
||||
function useDebounce<T extends (...args: any[]) => any>(callback: T, delay: number): T;
|
||||
function useDebounce<T>(value: T, delay: number): T;
|
||||
|
||||
function useDebounce<T>(value: T, delay: number): T {
|
||||
const isFn = typeof value === 'function';
|
||||
|
||||
const debouncedFunction = useMemo(() => {
|
||||
return isFn ? debounce(value as unknown as (...args: any[]) => any, delay) : null;
|
||||
}, [isFn, value, delay]);
|
||||
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = debounce((val: T) => {
|
||||
setDebouncedValue(val);
|
||||
}, delay);
|
||||
if (!isFn) {
|
||||
const handler = setTimeout(() => setDebouncedValue(value), delay);
|
||||
return () => clearTimeout(handler);
|
||||
}
|
||||
}, [isFn, value, delay]);
|
||||
|
||||
handler(value);
|
||||
|
||||
|
||||
return () => {
|
||||
handler.cancel();
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
};
|
||||
return isFn ? (debouncedFunction as unknown as T) : debouncedValue;
|
||||
}
|
||||
|
||||
export default useDebounce;
|
||||
|
|
|
@ -7,7 +7,12 @@ import { requestCameraAuthorization } from '../helpers/scan-qr';
|
|||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
// List of screens that require biometrics
|
||||
const requiresBiometrics = ['WalletExportRoot', 'WalletXpubRoot', 'ViewEditMultisigCosignersRoot', 'ExportMultisigCoordinationSetupRoot'];
|
||||
const requiresBiometrics = [
|
||||
'WalletExportRoot',
|
||||
'WalletXpubRoot',
|
||||
'ViewEditMultisigCosigners',
|
||||
'ExportMultisigCoordinationSetupRoot',
|
||||
];
|
||||
|
||||
// List of screens that require wallet export to be saved
|
||||
const requiresWalletExportIsSaved = ['ReceiveDetailsRoot', 'WalletAddresses'];
|
||||
|
@ -17,8 +22,25 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
|
|||
const { wallets, saveToDisk } = useStorage();
|
||||
const { isBiometricUseEnabled } = useBiometrics();
|
||||
|
||||
const enhancedNavigate: NavigationProp<ParamListBase>['navigate'] = useCallback(
|
||||
(screenOrOptions: any, params?: any, options?: { merge?: boolean }) => {
|
||||
const enhancedNavigate = useCallback(
|
||||
(
|
||||
...args:
|
||||
| [string]
|
||||
| [string, object | undefined]
|
||||
| [string, object | undefined, { merge?: boolean }]
|
||||
| [{ name: string; params?: object; path?: string; merge?: boolean }]
|
||||
) => {
|
||||
let screenOrOptions: any;
|
||||
let params: any;
|
||||
let options: { merge?: boolean } | undefined;
|
||||
|
||||
if (typeof args[0] === 'string') {
|
||||
screenOrOptions = args[0];
|
||||
params = args[1];
|
||||
options = args[2];
|
||||
} else {
|
||||
screenOrOptions = args[0];
|
||||
}
|
||||
let screenName: string;
|
||||
if (typeof screenOrOptions === 'string') {
|
||||
screenName = screenOrOptions;
|
||||
|
@ -44,6 +66,13 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
|
|||
};
|
||||
|
||||
(async () => {
|
||||
// NEW: If the current (active) screen is 'ScanQRCode', bypass all checks.
|
||||
const currentRouteName = navigationRef.current?.getCurrentRoute()?.name;
|
||||
if (currentRouteName === 'ScanQRCode') {
|
||||
proceedWithNavigation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRequiresBiometrics) {
|
||||
const isBiometricsEnabled = await isBiometricUseEnabled();
|
||||
if (isBiometricsEnabled) {
|
||||
|
@ -53,8 +82,8 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
|
|||
return;
|
||||
} else {
|
||||
console.error('Biometric authentication failed');
|
||||
// Decide if navigation should proceed or not after failed authentication
|
||||
return; // Prevent proceeding with the original navigation if bio fails
|
||||
// Do not proceed if authentication fails.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,18 +107,17 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
|
|||
await saveToDisk();
|
||||
proceedWithNavigation();
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
originalNavigation.navigate('WalletExportRoot', {
|
||||
screen: 'WalletExport',
|
||||
params: { walletID },
|
||||
});
|
||||
}
|
||||
// If there was an error (or the user cancelled), navigate to the wallet export screen.
|
||||
originalNavigation.navigate('WalletExportRoot', {
|
||||
screen: 'WalletExport',
|
||||
params: { walletID },
|
||||
});
|
||||
}
|
||||
|
||||
return; // Prevent proceeding with the original navigation if the reminder is shown
|
||||
return; // Do not proceed with the original navigation if reminder was shown.
|
||||
}
|
||||
}
|
||||
|
||||
// If the target screen is ScanQRCode, request camera authorization.
|
||||
if (screenName === 'ScanQRCode') {
|
||||
await requestCameraAuthorization();
|
||||
}
|
||||
|
@ -115,4 +143,4 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
|
|||
|
||||
// Usage example:
|
||||
// type NavigationProps = NativeStackNavigationProp<SendDetailsStackParamList, 'SendDetails'>;
|
||||
// const navigation = useExtendedNavigation<NavigationProps>();
|
||||
// const navigation = useExtendedNavigation<NavigationProps>();
|
|
@ -23,20 +23,25 @@ const useHandoffListener = () => {
|
|||
|
||||
const handleUserActivity = useCallback(
|
||||
(data: UserActivityData) => {
|
||||
if (!data || !data.activityType) {
|
||||
console.debug(`Invalid handoff data received: ${data ? JSON.stringify(data) : 'No data provided'}`);
|
||||
return;
|
||||
}
|
||||
const { activityType, userInfo } = data;
|
||||
const modifiedUserInfo = { ...(userInfo || {}), type: activityType };
|
||||
try {
|
||||
if (activityType === HandOffActivityType.ReceiveOnchain) {
|
||||
if (activityType === HandOffActivityType.ReceiveOnchain && modifiedUserInfo.address) {
|
||||
navigate('ReceiveDetailsRoot', {
|
||||
screen: 'ReceiveDetails',
|
||||
params: { address: userInfo.address },
|
||||
params: { address: modifiedUserInfo.address, type: activityType },
|
||||
});
|
||||
} else if (activityType === HandOffActivityType.Xpub) {
|
||||
} else if (activityType === HandOffActivityType.Xpub && modifiedUserInfo.xpub) {
|
||||
navigate('WalletXpubRoot', {
|
||||
screen: 'WalletXpub',
|
||||
params: { xpub: userInfo.xpub },
|
||||
params: { xpub: modifiedUserInfo.xpub, type: activityType },
|
||||
});
|
||||
} else {
|
||||
console.debug(`Unhandled activity type: ${activityType}`);
|
||||
console.debug(`Unhandled or incomplete activity type/data: ${activityType}`, modifiedUserInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling user activity:', error);
|
||||
|
@ -50,9 +55,13 @@ const useHandoffListener = () => {
|
|||
|
||||
const activitySubscription = eventEmitter?.addListener('onUserActivityOpen', handleUserActivity);
|
||||
|
||||
EventEmitter.getMostRecentUserActivity?.()
|
||||
.then(handleUserActivity)
|
||||
.catch(() => console.debug('No userActivity object sent'));
|
||||
if (EventEmitter && EventEmitter.getMostRecentUserActivity) {
|
||||
EventEmitter.getMostRecentUserActivity()
|
||||
.then(handleUserActivity)
|
||||
.catch(() => console.debug('No valid user activity object received'));
|
||||
} else {
|
||||
console.debug('EventEmitter native module is not available.');
|
||||
}
|
||||
|
||||
return () => {
|
||||
activitySubscription?.remove();
|
||||
|
|
|
@ -1,68 +1,168 @@
|
|||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
|
||||
import { navigationRef } from '../NavigationService';
|
||||
import { CommonActions } from '@react-navigation/native';
|
||||
import * as NavigationService from '../NavigationService';
|
||||
import { useStorage } from './context/useStorage';
|
||||
|
||||
/*
|
||||
Hook for managing iPadOS and macOS menu actions with keyboard shortcuts.
|
||||
Uses MenuElementsEmitter for event handling.
|
||||
Uses MenuElementsEmitter for event handling and navigation state.
|
||||
*/
|
||||
|
||||
type MenuActionHandler = () => void;
|
||||
|
||||
// Singleton setup - initialize once at module level
|
||||
const { MenuElementsEmitter } = NativeModules;
|
||||
const eventEmitter =
|
||||
(Platform.OS === 'ios' || Platform.OS === 'macos') && MenuElementsEmitter ? new NativeEventEmitter(MenuElementsEmitter) : null;
|
||||
let eventEmitter: NativeEventEmitter | null = null;
|
||||
let listenersInitialized = false;
|
||||
|
||||
const useMenuElements = () => {
|
||||
const { walletsInitialized } = useStorage();
|
||||
const reloadTransactionsMenuActionRef = useRef<() => void>(() => {});
|
||||
// Registry for transaction handlers by screen ID
|
||||
const handlerRegistry = new Map<string, MenuActionHandler>();
|
||||
|
||||
const setReloadTransactionsMenuActionFunction = useCallback((newFunction: () => void) => {
|
||||
console.debug('Setting reloadTransactionsMenuActionFunction.');
|
||||
reloadTransactionsMenuActionRef.current = newFunction;
|
||||
}, []);
|
||||
// Store subscription references for proper cleanup
|
||||
let subscriptions: { remove: () => void }[] = [];
|
||||
|
||||
const dispatchNavigate = useCallback((routeName: string, screen?: string) => {
|
||||
NavigationService.dispatch(CommonActions.navigate({ name: routeName, params: screen ? { screen } : undefined }));
|
||||
}, []);
|
||||
// Create a more robust emitter with error handling
|
||||
try {
|
||||
if (Platform.OS === 'ios' && MenuElementsEmitter) {
|
||||
eventEmitter = new NativeEventEmitter(MenuElementsEmitter);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[MenuElements] Failed to initialize event emitter: ', error);
|
||||
eventEmitter = null;
|
||||
}
|
||||
|
||||
const eventActions = useMemo(
|
||||
() => ({
|
||||
openSettings: () => dispatchNavigate('Settings'),
|
||||
addWallet: () => dispatchNavigate('AddWalletRoot'),
|
||||
importWallet: () => dispatchNavigate('AddWalletRoot', 'ImportWallet'),
|
||||
reloadTransactions: () => {
|
||||
console.debug('Calling reloadTransactionsMenuActionFunction');
|
||||
reloadTransactionsMenuActionRef.current?.();
|
||||
},
|
||||
}),
|
||||
[dispatchNavigate],
|
||||
);
|
||||
/**
|
||||
* Safely navigate using multiple fallback approaches
|
||||
*/
|
||||
function safeNavigate(routeName: string, params?: Record<string, any>): void {
|
||||
try {
|
||||
if (navigationRef.current?.isReady()) {
|
||||
navigationRef.current.navigate(routeName as never, params as never);
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.dispatch(
|
||||
CommonActions.navigate({
|
||||
name: routeName,
|
||||
params,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[MenuElements] Navigation error:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup event listeners to prevent memory leaks
|
||||
function cleanupListeners(): void {
|
||||
if (subscriptions.length > 0) {
|
||||
subscriptions.forEach(subscription => {
|
||||
try {
|
||||
subscription.remove();
|
||||
} catch (e) {
|
||||
console.warn('[MenuElements] Error removing subscription:', e);
|
||||
}
|
||||
});
|
||||
subscriptions = [];
|
||||
listenersInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
function initializeListeners(): void {
|
||||
if (!eventEmitter || listenersInitialized) return;
|
||||
|
||||
cleanupListeners();
|
||||
|
||||
// Navigation actions
|
||||
const globalActions = {
|
||||
navigateToSettings: (): void => {
|
||||
safeNavigate('Settings');
|
||||
},
|
||||
|
||||
navigateToAddWallet: (): void => {
|
||||
safeNavigate('AddWalletRoot');
|
||||
},
|
||||
|
||||
navigateToImportWallet: (): void => {
|
||||
safeNavigate('AddWalletRoot', { screen: 'ImportWallet' });
|
||||
},
|
||||
|
||||
executeReloadTransactions: (): void => {
|
||||
const currentRoute = navigationRef.current?.getCurrentRoute();
|
||||
if (!currentRoute) return;
|
||||
|
||||
const screenName = currentRoute.name;
|
||||
const params = (currentRoute.params as { walletID?: string }) || {};
|
||||
const walletID = params.walletID;
|
||||
|
||||
const specificKey = walletID ? `${screenName}-${walletID}` : null;
|
||||
|
||||
const specificHandler = specificKey ? handlerRegistry.get(specificKey) : undefined;
|
||||
const genericHandler = handlerRegistry.get(screenName);
|
||||
const handler = specificHandler || genericHandler;
|
||||
|
||||
if (typeof handler === 'function') {
|
||||
handler();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
subscriptions.push(eventEmitter.addListener('openSettings', globalActions.navigateToSettings));
|
||||
subscriptions.push(eventEmitter.addListener('addWalletMenuAction', globalActions.navigateToAddWallet));
|
||||
subscriptions.push(eventEmitter.addListener('importWalletMenuAction', globalActions.navigateToImportWallet));
|
||||
subscriptions.push(eventEmitter.addListener('reloadTransactionsMenuAction', globalActions.executeReloadTransactions));
|
||||
} catch (error) {
|
||||
console.error('[MenuElements] Error setting up event listeners:', error);
|
||||
}
|
||||
|
||||
listenersInitialized = true;
|
||||
}
|
||||
|
||||
interface MenuElementsHook {
|
||||
registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean;
|
||||
unregisterTransactionsHandler: (screenKey: string) => void;
|
||||
isMenuElementsSupported: boolean;
|
||||
}
|
||||
|
||||
const mountedComponents = new Set<string>();
|
||||
|
||||
const useMenuElements = (): MenuElementsHook => {
|
||||
useEffect(() => {
|
||||
if (!walletsInitialized || !eventEmitter) return;
|
||||
initializeListeners();
|
||||
|
||||
console.debug('Setting up menu event listeners');
|
||||
|
||||
// Add permanent listeners only once
|
||||
eventEmitter.removeAllListeners('openSettings');
|
||||
eventEmitter.removeAllListeners('addWalletMenuAction');
|
||||
eventEmitter.removeAllListeners('importWalletMenuAction');
|
||||
|
||||
eventEmitter.addListener('openSettings', eventActions.openSettings);
|
||||
eventEmitter.addListener('addWalletMenuAction', eventActions.addWallet);
|
||||
eventEmitter.addListener('importWalletMenuAction', eventActions.importWallet);
|
||||
|
||||
const reloadTransactionsListener = eventEmitter.addListener('reloadTransactionsMenuAction', eventActions.reloadTransactions);
|
||||
const unsubscribe = navigationRef.addListener('state', () => {});
|
||||
|
||||
return () => {
|
||||
console.debug('Removing reloadTransactionsMenuAction listener');
|
||||
reloadTransactionsListener.remove();
|
||||
unsubscribe();
|
||||
};
|
||||
}, [walletsInitialized, eventActions]);
|
||||
}, []);
|
||||
|
||||
const registerTransactionsHandler = useCallback((handler: MenuActionHandler, screenKey?: string): boolean => {
|
||||
if (typeof handler !== 'function') return false;
|
||||
|
||||
const key = screenKey || navigationRef.current?.getCurrentRoute()?.name;
|
||||
if (!key) return false;
|
||||
|
||||
mountedComponents.add(key);
|
||||
|
||||
handlerRegistry.set(key, handler);
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const unregisterTransactionsHandler = useCallback((screenKey: string): void => {
|
||||
if (!screenKey) return;
|
||||
|
||||
handlerRegistry.delete(screenKey);
|
||||
mountedComponents.delete(screenKey);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
setReloadTransactionsMenuActionFunction,
|
||||
registerTransactionsHandler,
|
||||
unregisterTransactionsHandler,
|
||||
isMenuElementsSupported: !!eventEmitter,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,8 +1,28 @@
|
|||
const useMenuElements = () => {
|
||||
const setReloadTransactionsMenuActionFunction = (_: () => void) => {};
|
||||
import { useCallback } from 'react';
|
||||
|
||||
type MenuActionHandler = () => void;
|
||||
|
||||
interface MenuElementsHook {
|
||||
registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean;
|
||||
unregisterTransactionsHandler: (screenKey: string) => void;
|
||||
isMenuElementsSupported: boolean;
|
||||
}
|
||||
|
||||
// Default implementation for platforms other than iOS
|
||||
const useMenuElements = (): MenuElementsHook => {
|
||||
const registerTransactionsHandler = useCallback((_handler: MenuActionHandler, _screenKey?: string): boolean => {
|
||||
// Non-functional stub for non-iOS platforms
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
const unregisterTransactionsHandler = useCallback((_screenKey: string): void => {
|
||||
// No-op for non-supported platforms
|
||||
}, []);
|
||||
|
||||
return {
|
||||
setReloadTransactionsMenuActionFunction,
|
||||
registerTransactionsHandler,
|
||||
unregisterTransactionsHandler,
|
||||
isMenuElementsSupported: false, // Not supported on platforms other than iOS
|
||||
};
|
||||
};
|
||||
|
||||
|
|
8
index.js
8
index.js
|
@ -1,3 +1,4 @@
|
|||
import './gesture-handler';
|
||||
import './shim.js';
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
|
@ -12,7 +13,12 @@ if (!Error.captureStackTrace) {
|
|||
Error.captureStackTrace = () => {};
|
||||
}
|
||||
|
||||
LogBox.ignoreLogs(['Require cycle:', 'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.']);
|
||||
LogBox.ignoreLogs([
|
||||
'Require cycle:',
|
||||
'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.',
|
||||
'Open debugger to view warnings.',
|
||||
'Non-serializable values were found in the navigation state',
|
||||
]);
|
||||
|
||||
const BlueAppComponent = () => {
|
||||
useEffect(() => {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objectVersion = 63;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
|
@ -14,7 +14,6 @@
|
|||
32F0A29A2311DBB20095C559 /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F0A2992311DBB20095C559 /* ComplicationController.swift */; };
|
||||
6D2A6464258BA92D0092292B /* Stickers.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6D2A6463258BA92D0092292B /* Stickers.xcassets */; };
|
||||
6D2A6468258BA92D0092292B /* Stickers.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6D2A6461258BA92C0092292B /* Stickers.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
6D32C5C62596CE3A008C077C /* EventEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D32C5C52596CE3A008C077C /* EventEmitter.m */; };
|
||||
6D4AF15925D21172009DD853 /* MarketAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D9A2E6A254BAB1B007B5B82 /* MarketAPI.swift */; };
|
||||
6D4AF16D25D21192009DD853 /* Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEB4BFA254FBA0E00E9F9AA /* Placeholders.swift */; };
|
||||
6D4AF17825D211A3009DD853 /* FiatUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D2AA8072568B8F40090B089 /* FiatUnit.swift */; };
|
||||
|
@ -46,6 +45,8 @@
|
|||
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B9D9B3A7B2CB4255876B67AF /* libz.tbd */; };
|
||||
849047CA2702A32A008EE567 /* Handoff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849047C92702A32A008EE567 /* Handoff.swift */; };
|
||||
84E05A842721191B001A0D3A /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 84E05A832721191B001A0D3A /* Settings.bundle */; };
|
||||
B409AB042D71DFAA00BA06F8 /* MenuElementsEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = B409AB032D71DFAA00BA06F8 /* MenuElementsEmitter.m */; };
|
||||
B409AB062D71E07500BA06F8 /* MenuElementsEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B409AB052D71E07500BA06F8 /* MenuElementsEmitter.swift */; };
|
||||
B40D4E34225841EC00428FCC /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E32225841EC00428FCC /* Interface.storyboard */; };
|
||||
B40D4E36225841ED00428FCC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; };
|
||||
B40D4E3D225841ED00428FCC /* BlueWalletWatch Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = B40D4E3C225841ED00428FCC /* BlueWalletWatch Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -113,12 +114,11 @@
|
|||
B440340F2BCC40A400162242 /* fiatUnits.json in Resources */ = {isa = PBXBuildFile; fileRef = B440340E2BCC40A400162242 /* fiatUnits.json */; };
|
||||
B44034102BCC40A400162242 /* fiatUnits.json in Resources */ = {isa = PBXBuildFile; fileRef = B440340E2BCC40A400162242 /* fiatUnits.json */; };
|
||||
B44034112BCC40A400162242 /* fiatUnits.json in Resources */ = {isa = PBXBuildFile; fileRef = B440340E2BCC40A400162242 /* fiatUnits.json */; };
|
||||
B44305BC2D6A04B2004675CC /* CustomSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = B44305BB2D6A04B2004675CC /* CustomSegmentedControl.m */; };
|
||||
B450109C2C0FCD8A00619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
|
||||
B450109D2C0FCD9F00619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
|
||||
B450109F2C0FCDA500619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
|
||||
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */; };
|
||||
B4549F362B82B10D002E3153 /* ci_post_clone.sh in Resources */ = {isa = PBXBuildFile; fileRef = B4549F352B82B10D002E3153 /* ci_post_clone.sh */; };
|
||||
B45942C42CDECF2400B3DC2E /* MenuElementsEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = B4C075292CDDB3C500322A84 /* MenuElementsEmitter.m */; };
|
||||
B461B852299599F800E431AA /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = B461B851299599F800E431AA /* AppDelegate.mm */; };
|
||||
B4742E972CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
|
||||
B4742E982CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
|
||||
|
@ -156,6 +156,8 @@
|
|||
B4AB225E2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; };
|
||||
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; };
|
||||
B4B1A4642BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; };
|
||||
B4B3EC222D69FF6C00327F3D /* CustomSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */; };
|
||||
B4B3EC252D69FF8700327F3D /* EventEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EC232D69FF8700327F3D /* EventEmitter.swift */; };
|
||||
B4D0B2622C1DEA11006B6B1B /* ReceivePageInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2612C1DEA11006B6B1B /* ReceivePageInterfaceController.swift */; };
|
||||
B4D0B2642C1DEA99006B6B1B /* ReceiveType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2632C1DEA99006B6B1B /* ReceiveType.swift */; };
|
||||
B4D0B2662C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */; };
|
||||
|
@ -294,8 +296,6 @@
|
|||
6D2A6463258BA92D0092292B /* Stickers.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Stickers.xcassets; sourceTree = "<group>"; };
|
||||
6D2A6465258BA92D0092292B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
6D2AA8072568B8F40090B089 /* FiatUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiatUnit.swift; sourceTree = "<group>"; };
|
||||
6D32C5C42596CE2F008C077C /* EventEmitter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EventEmitter.h; sourceTree = "<group>"; };
|
||||
6D32C5C52596CE3A008C077C /* EventEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EventEmitter.m; sourceTree = "<group>"; };
|
||||
6D333B3A252FE1A3004D72DF /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
6D333B3C252FE1A3004D72DF /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||
6D4AF18225D215D0009DD853 /* BlueWalletWatch-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BlueWalletWatch-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
|
@ -336,6 +336,8 @@
|
|||
9F1F51A83D044F3BB26A35FC /* libRNSVG-tvOS.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = "libRNSVG-tvOS.a"; sourceTree = "<group>"; };
|
||||
A7C4B1FDAD264618BAF8C335 /* libRNCWebView.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNCWebView.a; sourceTree = "<group>"; };
|
||||
AB2325650CE04F018697ACFE /* libRNReactNativeHapticFeedback.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNReactNativeHapticFeedback.a; sourceTree = "<group>"; };
|
||||
B409AB032D71DFAA00BA06F8 /* MenuElementsEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = MenuElementsEmitter.m; path = MenuElementsEmitter/MenuElementsEmitter.m; sourceTree = SOURCE_ROOT; };
|
||||
B409AB052D71E07500BA06F8 /* MenuElementsEmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MenuElementsEmitter.swift; path = MenuElementsEmitter/MenuElementsEmitter.swift; sourceTree = SOURCE_ROOT; };
|
||||
B40D4E30225841EC00428FCC /* BlueWalletWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BlueWalletWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B40D4E33225841EC00428FCC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = "<group>"; };
|
||||
B40D4E35225841ED00428FCC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
|
@ -372,9 +374,8 @@
|
|||
B44033F82BCC379200162242 /* WidgetDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDataStore.swift; sourceTree = "<group>"; };
|
||||
B44033FF2BCC37F800162242 /* Bundle+decode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+decode.swift"; sourceTree = "<group>"; };
|
||||
B440340E2BCC40A400162242 /* fiatUnits.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = fiatUnits.json; path = ../../../models/fiatUnits.json; sourceTree = "<group>"; };
|
||||
B44305BB2D6A04B2004675CC /* CustomSegmentedControl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomSegmentedControl.m; sourceTree = "<group>"; };
|
||||
B450109B2C0FCD8A00619044 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
|
||||
B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomSegmentedControlManager.m; sourceTree = "<group>"; };
|
||||
B45010A92C15080500619044 /* CustomSegmentedControlManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomSegmentedControlManager.h; sourceTree = "<group>"; };
|
||||
B4549F352B82B10D002E3153 /* ci_post_clone.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = ci_post_clone.sh; sourceTree = "<group>"; };
|
||||
B461B850299599F800E431AA /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = BlueWallet/AppDelegate.h; sourceTree = "<group>"; };
|
||||
B461B851299599F800E431AA /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = BlueWallet/AppDelegate.mm; sourceTree = "<group>"; };
|
||||
|
@ -396,8 +397,8 @@
|
|||
B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLParserDelegate.swift; sourceTree = "<group>"; };
|
||||
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetHelper.swift; sourceTree = "<group>"; };
|
||||
B4B31A352C77BBA000663334 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Interface.strings; sourceTree = "<group>"; };
|
||||
B4C075282CDDB3BE00322A84 /* MenuElementsEmitter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MenuElementsEmitter.h; sourceTree = "<group>"; };
|
||||
B4C075292CDDB3C500322A84 /* MenuElementsEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MenuElementsEmitter.m; sourceTree = "<group>"; };
|
||||
B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSegmentedControl.swift; sourceTree = "<group>"; };
|
||||
B4B3EC232D69FF8700327F3D /* EventEmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventEmitter.swift; sourceTree = "<group>"; };
|
||||
B4D0B2612C1DEA11006B6B1B /* ReceivePageInterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceivePageInterfaceController.swift; sourceTree = "<group>"; };
|
||||
B4D0B2632C1DEA99006B6B1B /* ReceiveType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveType.swift; sourceTree = "<group>"; };
|
||||
B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveInterfaceMode.swift; sourceTree = "<group>"; };
|
||||
|
@ -489,7 +490,6 @@
|
|||
13B07FAE1A68108700A75B9A /* BlueWallet */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B4C0752B2CDDB3CC00322A84 /* MenuElementsEmitter */,
|
||||
B461B850299599F800E431AA /* AppDelegate.h */,
|
||||
B461B851299599F800E431AA /* AppDelegate.mm */,
|
||||
32C7944323B8879D00BE2AFA /* BlueWalletRelease.entitlements */,
|
||||
|
@ -501,8 +501,6 @@
|
|||
32B5A3292334450100F8D608 /* Bridge.swift */,
|
||||
32B5A3282334450100F8D608 /* BlueWallet-Bridging-Header.h */,
|
||||
6DF25A9E249DB97E001D06F5 /* LaunchScreen.storyboard */,
|
||||
6D32C5C42596CE2F008C077C /* EventEmitter.h */,
|
||||
6D32C5C52596CE3A008C077C /* EventEmitter.m */,
|
||||
84E05A832721191B001A0D3A /* Settings.bundle */,
|
||||
B4742E962CCDBE8300380EEE /* Localizable.xcstrings */,
|
||||
);
|
||||
|
@ -677,6 +675,15 @@
|
|||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B409AB072D71E07C00BA06F8 /* MenuElementsEmitter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B409AB052D71E07500BA06F8 /* MenuElementsEmitter.swift */,
|
||||
B409AB032D71DFAA00BA06F8 /* MenuElementsEmitter.m */,
|
||||
);
|
||||
path = MenuElementsEmitter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B40D4E31225841EC00428FCC /* BlueWalletWatch */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -786,6 +793,15 @@
|
|||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B44305BD2D6A04B9004675CC /* SegmentedControl */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B44305BB2D6A04B2004675CC /* CustomSegmentedControl.m */,
|
||||
B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */,
|
||||
);
|
||||
path = SegmentedControl;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B450109A2C0FCD7E00619044 /* Utilities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -798,21 +814,14 @@
|
|||
B45010A12C1504E900619044 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B409AB072D71E07C00BA06F8 /* MenuElementsEmitter */,
|
||||
B44305BD2D6A04B9004675CC /* SegmentedControl */,
|
||||
B4B3EC232D69FF8700327F3D /* EventEmitter.swift */,
|
||||
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */,
|
||||
B45010A82C1507F000619044 /* SegmentedControl */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B45010A82C1507F000619044 /* SegmentedControl */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */,
|
||||
B45010A92C15080500619044 /* CustomSegmentedControlManager.h */,
|
||||
);
|
||||
path = SegmentedControl;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B4549F2E2B80FEA1002E3153 /* ci_scripts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -829,15 +838,6 @@
|
|||
path = BlueWalletUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B4C0752B2CDDB3CC00322A84 /* MenuElementsEmitter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B4C075292CDDB3C500322A84 /* MenuElementsEmitter.m */,
|
||||
B4C075282CDDB3BE00322A84 /* MenuElementsEmitter.h */,
|
||||
);
|
||||
path = MenuElementsEmitter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FAA856B639C61E61D2CF90A8 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -993,6 +993,7 @@
|
|||
};
|
||||
};
|
||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "BlueWallet" */;
|
||||
compatibilityVersion = "Xcode 15.3";
|
||||
developmentRegion = en_US;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
|
@ -1029,7 +1030,6 @@
|
|||
6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */,
|
||||
B41B76832B66B2FF002C48D5 /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
|
@ -1239,28 +1239,30 @@
|
|||
B44033EE2BCC374500162242 /* Numeric+abbreviated.swift in Sources */,
|
||||
B48630E82CCEE92400A8425C /* PriceWidget.swift in Sources */,
|
||||
B44033DD2BCC36C300162242 /* LatestTransaction.swift in Sources */,
|
||||
6D32C5C62596CE3A008C077C /* EventEmitter.m in Sources */,
|
||||
B49A28C12CD199FC006B08E4 /* SwiftTCPClient.swift in Sources */,
|
||||
B44033FE2BCC37D700162242 /* MarketAPI.swift in Sources */,
|
||||
B450109C2C0FCD8A00619044 /* Utilities.swift in Sources */,
|
||||
B409AB042D71DFAA00BA06F8 /* MenuElementsEmitter.m in Sources */,
|
||||
B48630E52CCEE8B800A8425C /* PriceView.swift in Sources */,
|
||||
B48630E72CCEE91900A8425C /* PriceWidgetProvider.swift in Sources */,
|
||||
B4B3EC252D69FF8700327F3D /* EventEmitter.swift in Sources */,
|
||||
B49A28C02CD199C7006B08E4 /* MarketAPI+Electrum.swift in Sources */,
|
||||
B48630ED2CCEEEB000A8425C /* WalletAppShortcuts.swift in Sources */,
|
||||
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */,
|
||||
B409AB062D71E07500BA06F8 /* MenuElementsEmitter.swift in Sources */,
|
||||
B44033CE2BCC352900162242 /* UserDefaultsGroup.swift in Sources */,
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||
B45942C42CDECF2400B3DC2E /* MenuElementsEmitter.m in Sources */,
|
||||
B461B852299599F800E431AA /* AppDelegate.mm in Sources */,
|
||||
B44033F42BCC377F00162242 /* WidgetData.swift in Sources */,
|
||||
B49A28C52CD1A894006B08E4 /* MarketData.swift in Sources */,
|
||||
B49A28BF2CD18A9A006B08E4 /* FiatUnitEnum.swift in Sources */,
|
||||
B44305BC2D6A04B2004675CC /* CustomSegmentedControl.m in Sources */,
|
||||
B44033C42BCC332400162242 /* Balance.swift in Sources */,
|
||||
B48630EE2CCEEEE900A8425C /* PriceIntent.swift in Sources */,
|
||||
B44034072BCC38A000162242 /* FiatUnit.swift in Sources */,
|
||||
B44034002BCC37F800162242 /* Bundle+decode.swift in Sources */,
|
||||
B44033E22BCC36CB00162242 /* Placeholders.swift in Sources */,
|
||||
B4793DBB2CEDACBD00C92C2E /* Chain.swift in Sources */,
|
||||
B4B3EC222D69FF6C00327F3D /* CustomSegmentedControl.swift in Sources */,
|
||||
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */,
|
||||
B48630E12CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */,
|
||||
B44033DA2BCC369A00162242 /* Colors.swift in Sources */,
|
||||
|
@ -1453,7 +1455,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703136999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
|
||||
|
@ -1471,7 +1473,7 @@
|
|||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
|
||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
|
||||
INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -1481,7 +1483,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.8;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@ -1516,7 +1518,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703136999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
|
||||
|
@ -1529,7 +1531,7 @@
|
|||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
|
||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
|
||||
INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -1539,7 +1541,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.8;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@ -1575,20 +1577,20 @@
|
|||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703136999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = Stickers/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/usr/lib/swift",
|
||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.8;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -1618,20 +1620,20 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1703136999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = Stickers/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/usr/lib/swift",
|
||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.8;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers;
|
||||
|
@ -1662,7 +1664,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703136999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -1681,7 +1683,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.8;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -1718,7 +1720,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1703136999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -1737,7 +1739,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.8;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget;
|
||||
|
@ -1905,7 +1907,7 @@
|
|||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703136999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -1925,7 +1927,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.8;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -1958,7 +1960,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1703136999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -1978,7 +1980,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.8;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension;
|
||||
|
@ -2010,7 +2012,7 @@
|
|||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703136999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -2024,7 +2026,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.8;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -2059,7 +2061,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1703136999;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -2073,7 +2075,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.8;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch;
|
||||
|
|
|
@ -6,12 +6,9 @@
|
|||
#import "RNQuickActionManager.h"
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
#import <RNCPushNotificationIOS.h>
|
||||
#import "EventEmitter.h"
|
||||
#import "MenuElementsEmitter.h"
|
||||
#import <React/RCTRootView.h>
|
||||
#import <Bugsnag/Bugsnag.h>
|
||||
#import "BlueWallet-Swift.h"
|
||||
#import "CustomSegmentedControlManager.h"
|
||||
|
||||
@interface AppDelegate() <UNUserNotificationCenterDelegate>
|
||||
|
||||
|
@ -23,8 +20,6 @@
|
|||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
[MenuElementsEmitter sharedInstance];
|
||||
[CustomSegmentedControlManager registerIfNecessary];
|
||||
[self clearFilesIfNeeded];
|
||||
self.userDefaultsGroup = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"];
|
||||
|
||||
|
@ -154,27 +149,42 @@
|
|||
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity
|
||||
restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
|
||||
{
|
||||
NSDictionary *userActivityData = @{@"activityType": userActivity.activityType, @"userInfo": userActivity.userInfo};
|
||||
// Validate userActivity and its type
|
||||
if (!userActivity || !userActivity.activityType) {
|
||||
NSLog(@"[Handoff] Invalid or missing userActivity");
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSDictionary *userActivityData = @{@"activityType": userActivity.activityType ?: @"",
|
||||
@"userInfo": userActivity.userInfo ?: @{}};
|
||||
|
||||
// Save activity data to userDefaults for potential later use
|
||||
[self.userDefaultsGroup setValue:userActivityData forKey:@"onUserActivityOpen"];
|
||||
|
||||
// Check if the activity type matches the allowed types
|
||||
// Check if the activity type matches one of the allowed types
|
||||
if ([userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.receiveonchain"] ||
|
||||
[userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.xpub"] ||
|
||||
[userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.blockexplorer"]) {
|
||||
|
||||
[EventEmitter.sharedInstance sendUserActivity:userActivityData];
|
||||
if ([EventEmitter.shared respondsToSelector:@selector(sendUserActivity:)]) {
|
||||
[EventEmitter.shared sendUserActivity:userActivityData];
|
||||
} else {
|
||||
NSLog(@"[Handoff] EventEmitter does not implement sendUserActivity:");
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (userActivity.activityType == NSUserActivityTypeBrowsingWeb) {
|
||||
// Forward web browsing activities to LinkingManager
|
||||
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
|
||||
return [RCTLinkingManager application:application
|
||||
continueUserActivity:userActivity
|
||||
restorationHandler:restorationHandler];
|
||||
}
|
||||
|
||||
// If activity type does not match any of the specified types, do nothing
|
||||
NSLog(@"[Handoff] Unhandled user activity type: %@", userActivity.activityType);
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
|
||||
return [RCTLinkingManager application:app openURL:url options:options];
|
||||
}
|
||||
|
@ -195,7 +205,7 @@
|
|||
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
|
||||
{
|
||||
NSDictionary *userInfo = notification.request.content.userInfo;
|
||||
completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
|
||||
completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionBadge);
|
||||
}
|
||||
|
||||
- (void)buildMenuWithBuilder:(id<UIMenuBuilder>)builder {
|
||||
|
@ -244,25 +254,59 @@
|
|||
}
|
||||
|
||||
- (void)openSettings:(UIKeyCommand *)keyCommand {
|
||||
[MenuElementsEmitter.sharedInstance openSettings];
|
||||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: openSettings called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter openSettings];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for openSettings");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addWalletAction:(UIKeyCommand *)keyCommand {
|
||||
// Implement the functionality for adding a wallet
|
||||
[MenuElementsEmitter.sharedInstance addWalletMenuAction];
|
||||
NSLog(@"Add Wallet action performed");
|
||||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: addWalletAction called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter addWalletMenuAction];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for addWalletAction");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)importWalletAction:(UIKeyCommand *)keyCommand {
|
||||
// Implement the functionality for adding a wallet
|
||||
[MenuElementsEmitter.sharedInstance importWalletMenuAction];
|
||||
NSLog(@"Import Wallet action performed");
|
||||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: importWalletAction called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter importWalletMenuAction];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for importWalletAction");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reloadTransactionsAction:(UIKeyCommand *)keyCommand {
|
||||
// Implement the functionality for adding a wallet
|
||||
[MenuElementsEmitter.sharedInstance reloadTransactionsMenuAction];
|
||||
NSLog(@"Reload Transactions action performed");
|
||||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: reloadTransactionsAction called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter reloadTransactionsMenuAction];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for reloadTransactionsAction");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showHelp:(id)sender {
|
||||
|
|
32
ios/Components/EventEmitter.swift
Normal file
32
ios/Components/EventEmitter.swift
Normal file
|
@ -0,0 +1,32 @@
|
|||
import Foundation
|
||||
import React
|
||||
|
||||
@objc(EventEmitter)
|
||||
class EventEmitter: RCTEventEmitter {
|
||||
static let sharedInstance = EventEmitter()
|
||||
|
||||
override class func requiresMainQueueSetup() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@objc static func shared() -> EventEmitter {
|
||||
return sharedInstance
|
||||
}
|
||||
|
||||
override func supportedEvents() -> [String]! {
|
||||
return ["onUserActivityOpen"]
|
||||
}
|
||||
|
||||
@objc func sendUserActivity(_ userInfo: [String: Any]) {
|
||||
sendEvent(withName: "onUserActivityOpen", body: userInfo)
|
||||
}
|
||||
|
||||
@objc func getMostRecentUserActivity(_ resolve: @escaping RCTPromiseResolveBlock,
|
||||
rejecter reject: RCTPromiseRejectBlock) {
|
||||
if let defaults = UserDefaults(suiteName: "group.io.bluewallet.bluewallet") {
|
||||
resolve(defaults.value(forKey: "onUserActivityOpen"))
|
||||
} else {
|
||||
resolve(nil)
|
||||
}
|
||||
}
|
||||
}
|
9
ios/Components/SegmentedControl/CustomSegmentedControl.m
Normal file
9
ios/Components/SegmentedControl/CustomSegmentedControl.m
Normal file
|
@ -0,0 +1,9 @@
|
|||
#import <React/RCTViewManager.h>
|
||||
|
||||
@interface RCT_EXTERN_MODULE(CustomSegmentedControlManager, RCTViewManager)
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(values, NSArray)
|
||||
RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSNumber)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onChangeEvent, RCTDirectEventBlock)
|
||||
|
||||
@end
|
70
ios/Components/SegmentedControl/CustomSegmentedControl.swift
Normal file
70
ios/Components/SegmentedControl/CustomSegmentedControl.swift
Normal file
|
@ -0,0 +1,70 @@
|
|||
import UIKit
|
||||
import React
|
||||
|
||||
class CustomSegmentedControl: UISegmentedControl {
|
||||
@objc var onChangeEvent: RCTDirectEventBlock?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
addTarget(self, action: #selector(onChange(_:)), for: .valueChanged)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
addTarget(self, action: #selector(onChange(_:)), for: .valueChanged)
|
||||
}
|
||||
|
||||
@objc func setValues(_ values: [String]) {
|
||||
removeAllSegments()
|
||||
for (index, title) in values.enumerated() {
|
||||
insertSegment(withTitle: title, at: index, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setSelectedIndex(_ selectedIndex: NSNumber) {
|
||||
self.selectedSegmentIndex = selectedIndex.intValue
|
||||
}
|
||||
|
||||
@objc func onChange(_ sender: UISegmentedControl) {
|
||||
onChangeEvent?(["selectedIndex": sender.selectedSegmentIndex])
|
||||
}
|
||||
}
|
||||
|
||||
@objc(CustomSegmentedControlManager)
|
||||
class CustomSegmentedControlManager: RCTViewManager {
|
||||
static var isRegistered = false
|
||||
|
||||
override func view() -> UIView! {
|
||||
// Ensure native module is registered before returning the view.
|
||||
CustomSegmentedControlManager.registerIfNecessary()
|
||||
return CustomSegmentedControl(frame: .zero)
|
||||
}
|
||||
|
||||
@objc static func registerIfNecessary() {
|
||||
if !isRegistered {
|
||||
isRegistered = true
|
||||
}
|
||||
}
|
||||
|
||||
// Changed from static to instance method.
|
||||
override func constantsToExport() -> [AnyHashable: Any]! {
|
||||
return [
|
||||
"bubblingEventTypes": [
|
||||
"onChangeEvent": [
|
||||
"phasedRegistrationNames": [
|
||||
"bubbled": "onChangeEvent",
|
||||
"captured": "onChangeEventCapture"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
override class func moduleName() -> String! {
|
||||
return "CustomSegmentedControl"
|
||||
}
|
||||
|
||||
override class func requiresMainQueueSetup() -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
//
|
||||
// SegmentedControlManager.h
|
||||
// BlueWallet
|
||||
//
|
||||
// Created by Marcos Rodriguez on 6/8/24.
|
||||
// Copyright © 2024 BlueWallet. All rights reserved.
|
||||
//
|
||||
|
||||
#import <React/RCTViewManager.h>
|
||||
|
||||
@interface CustomSegmentedControlManager : RCTViewManager
|
||||
|
||||
+ (void)registerIfNecessary;
|
||||
|
||||
@end
|
|
@ -1,70 +0,0 @@
|
|||
#import "CustomSegmentedControlManager.h"
|
||||
#import <React/RCTBridge.h>
|
||||
#import <React/RCTEventDispatcher.h>
|
||||
#import <React/UIView+React.h>
|
||||
|
||||
@interface CustomSegmentedControl : UISegmentedControl
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onChangeEvent;
|
||||
- (void)setValues:(NSArray<NSString *> *)values;
|
||||
- (void)setSelectedIndex:(NSNumber *)selectedIndex;
|
||||
@end
|
||||
|
||||
@implementation CustomSegmentedControl
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setValues:(NSArray<NSString *> *)values {
|
||||
@try {
|
||||
[self removeAllSegments];
|
||||
for (NSUInteger i = 0; i < values.count; i++) {
|
||||
[self insertSegmentWithTitle:values[i] atIndex:i animated:NO];
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
NSLog(@"Error setting property 'values': %@", exception.reason);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setSelectedIndex:(NSNumber *)selectedIndex {
|
||||
@try {
|
||||
self.selectedSegmentIndex = selectedIndex.integerValue;
|
||||
} @catch (NSException *exception) {
|
||||
NSLog(@"Error setting property 'selectedIndex': %@", exception.reason);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onChange:(UISegmentedControl *)sender {
|
||||
if (self.onChangeEvent) {
|
||||
self.onChangeEvent(@{@"selectedIndex": @(self.selectedSegmentIndex)});
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CustomSegmentedControlManager
|
||||
|
||||
static BOOL isRegistered = NO;
|
||||
|
||||
RCT_EXPORT_MODULE(CustomSegmentedControl)
|
||||
|
||||
- (UIView *)view {
|
||||
return [CustomSegmentedControl new];
|
||||
}
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(values, NSArray)
|
||||
RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSNumber)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onChangeEvent, RCTDirectEventBlock)
|
||||
|
||||
+ (void)registerIfNecessary {
|
||||
if (!isRegistered) {
|
||||
isRegistered = YES;
|
||||
// Registration logic if necessary
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,17 +0,0 @@
|
|||
//
|
||||
// EventEmitter.h
|
||||
// BlueWallet
|
||||
//
|
||||
// Created by Marcos Rodriguez on 12/25/20.
|
||||
// Copyright © 2020 BlueWallet. All rights reserved.
|
||||
//
|
||||
|
||||
#import <React/RCTBridgeModule.h>
|
||||
#import <React/RCTEventEmitter.h>
|
||||
|
||||
@interface EventEmitter : RCTEventEmitter <RCTBridgeModule>
|
||||
|
||||
+ (EventEmitter *)sharedInstance;
|
||||
- (void)sendUserActivity:(NSDictionary *)userInfo;
|
||||
|
||||
@end
|
|
@ -1,54 +0,0 @@
|
|||
//
|
||||
// EventEmitter.m
|
||||
// BlueWallet
|
||||
//
|
||||
// Created by Marcos Rodriguez on 12/25/20.
|
||||
// Copyright © 2020 BlueWallet. All rights reserved.
|
||||
//
|
||||
|
||||
#import "EventEmitter.h"
|
||||
|
||||
static EventEmitter *sharedInstance;
|
||||
|
||||
@implementation EventEmitter
|
||||
|
||||
RCT_EXPORT_MODULE();
|
||||
|
||||
+ (BOOL)requiresMainQueueSetup {
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedInstance = [[self alloc] init];
|
||||
});
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
- (void)removeListeners:(double)count {
|
||||
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)supportedEvents {
|
||||
return @[@"onUserActivityOpen"];
|
||||
}
|
||||
|
||||
- (void)sendUserActivity:(NSDictionary *)userInfo
|
||||
{
|
||||
[self sendEventWithName:@"onUserActivityOpen" body:userInfo];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(getMostRecentUserActivity:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"];
|
||||
resolve([defaults valueForKey:@"onUserActivityOpen"]);
|
||||
}
|
||||
|
||||
@end
|
33
ios/EventEmitter.swift
Normal file
33
ios/EventEmitter.swift
Normal file
|
@ -0,0 +1,33 @@
|
|||
import Foundation
|
||||
import React
|
||||
|
||||
@objc(EventEmitter)
|
||||
class EventEmitter: RCTEventEmitter {
|
||||
static let sharedInstance = EventEmitter()
|
||||
|
||||
override class func requiresMainQueueSetup() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@objc static func shared() -> EventEmitter {
|
||||
return sharedInstance
|
||||
}
|
||||
|
||||
override func supportedEvents() -> [String]! {
|
||||
return ["onUserActivityOpen"]
|
||||
}
|
||||
|
||||
@objc func sendUserActivity(_ userInfo: [String: Any]) {
|
||||
// Removed unnecessary type check; directly sending the event with userInfo.
|
||||
sendEvent(withName: "onUserActivityOpen", body: userInfo)
|
||||
}
|
||||
|
||||
@objc func getMostRecentUserActivity(_ resolve: @escaping RCTPromiseResolveBlock,
|
||||
rejecter reject: RCTPromiseRejectBlock) {
|
||||
if let defaults = UserDefaults(suiteName: "group.io.bluewallet.bluewallet") {
|
||||
resolve(defaults.value(forKey: "onUserActivityOpen"))
|
||||
} else {
|
||||
resolve(nil)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
//
|
||||
// MenuElementsEmitter.h
|
||||
// BlueWallet
|
||||
//
|
||||
|
||||
#import <React/RCTEventEmitter.h>
|
||||
|
||||
@interface MenuElementsEmitter : RCTEventEmitter
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
- (void)openSettings;
|
||||
- (void)addWalletMenuAction;
|
||||
- (void)importWalletMenuAction;
|
||||
- (void)reloadTransactionsMenuAction;
|
||||
|
||||
@end
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// MenuElementsEmitter.h
|
||||
// BlueWallet
|
||||
//
|
||||
// Created by Marcos Rodriguez on 11/7/24.
|
||||
// Copyright © 2024 BlueWallet. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
//
|
||||
// MenuElementsEmitter.h
|
||||
// BlueWallet
|
||||
//
|
||||
|
||||
#import <React/RCTEventEmitter.h>
|
||||
|
||||
@interface MenuElementsEmitter : RCTEventEmitter
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
- (void)openSettings;
|
||||
- (void)addWalletMenuAction;
|
||||
- (void)importWalletMenuAction;
|
||||
- (void)reloadTransactionsMenuAction;
|
||||
- (NSArray<NSString *> *)supportedEvents;
|
||||
|
||||
@end
|
|
@ -1,62 +1,13 @@
|
|||
//
|
||||
// MenuElementsEmitter.m
|
||||
// BlueWallet
|
||||
//
|
||||
// Created by Marcos Rodriguez on 11/7/24.
|
||||
// Copyright © 2024 BlueWallet. All rights reserved.
|
||||
//
|
||||
#import <React/RCTBridgeModule.h>
|
||||
#import <React/RCTEventEmitter.h>
|
||||
|
||||
#import "MenuElementsEmitter.h"
|
||||
@interface RCT_EXTERN_MODULE(MenuElementsEmitter, RCTEventEmitter)
|
||||
|
||||
static MenuElementsEmitter *sharedInstance;
|
||||
|
||||
@implementation MenuElementsEmitter
|
||||
|
||||
RCT_EXPORT_MODULE();
|
||||
|
||||
+ (BOOL)requiresMainQueueSetup {
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedInstance = [[self alloc] init];
|
||||
});
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
sharedInstance = self;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)supportedEvents {
|
||||
return @[
|
||||
@"openSettings",
|
||||
@"addWalletMenuAction",
|
||||
@"importWalletMenuAction",
|
||||
@"reloadTransactionsMenuAction"
|
||||
];
|
||||
}
|
||||
|
||||
- (void)openSettings {
|
||||
[self sendEventWithName:@"openSettings" body:nil];
|
||||
}
|
||||
|
||||
- (void)addWalletMenuAction {
|
||||
[self sendEventWithName:@"addWalletMenuAction" body:nil];
|
||||
}
|
||||
|
||||
- (void)importWalletMenuAction {
|
||||
[self sendEventWithName:@"importWalletMenuAction" body:nil];
|
||||
}
|
||||
|
||||
- (void)reloadTransactionsMenuAction {
|
||||
[self sendEventWithName:@"reloadTransactionsMenuAction" body:nil];
|
||||
}
|
||||
RCT_EXTERN_METHOD(supportedEvents)
|
||||
RCT_EXTERN_METHOD(openSettings)
|
||||
RCT_EXTERN_METHOD(addWalletMenuAction)
|
||||
RCT_EXTERN_METHOD(importWalletMenuAction)
|
||||
RCT_EXTERN_METHOD(reloadTransactionsMenuAction)
|
||||
RCT_EXTERN_METHOD(shared)
|
||||
|
||||
@end
|
||||
|
|
134
ios/MenuElementsEmitter/MenuElementsEmitter.swift
Normal file
134
ios/MenuElementsEmitter/MenuElementsEmitter.swift
Normal file
|
@ -0,0 +1,134 @@
|
|||
import Foundation
|
||||
import React
|
||||
|
||||
@objc(MenuElementsEmitter)
|
||||
class MenuElementsEmitter: RCTEventEmitter {
|
||||
// Use a weak reference for the singleton to prevent retain cycles
|
||||
private static weak var sharedInstance: MenuElementsEmitter?
|
||||
|
||||
// Use LRU cache with a max size to prevent unbounded growth
|
||||
private var lastEventTime: [String: TimeInterval] = [:]
|
||||
private let throttleInterval: TimeInterval = 0.3 // 300ms throttle
|
||||
private let maxCacheSize = 10 // Limit the cache size
|
||||
|
||||
// Track listener state without needing constant bridge access
|
||||
private var hasListeners = false
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
MenuElementsEmitter.sharedInstance = self
|
||||
NSLog("[MenuElements] MenuElementsEmitter initialized")
|
||||
}
|
||||
|
||||
deinit {
|
||||
NSLog("[MenuElements] MenuElementsEmitter deallocated")
|
||||
// Ensure all event listeners are removed in deinit
|
||||
self.removeAllListeners()
|
||||
}
|
||||
|
||||
override class func requiresMainQueueSetup() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func supportedEvents() -> [String]! {
|
||||
return ["openSettings", "addWalletMenuAction", "importWalletMenuAction", "reloadTransactionsMenuAction"]
|
||||
}
|
||||
|
||||
@objc static func shared() -> MenuElementsEmitter? {
|
||||
if sharedInstance == nil {
|
||||
NSLog("[MenuElements] Warning: Attempting to use sharedInstance when it's nil")
|
||||
}
|
||||
return sharedInstance
|
||||
}
|
||||
|
||||
override func startObserving() {
|
||||
hasListeners = true
|
||||
NSLog("[MenuElements] Started observing events, bridge: \(self.bridge != nil ? "available" : "unavailable")")
|
||||
}
|
||||
|
||||
override func stopObserving() {
|
||||
hasListeners = false
|
||||
NSLog("[MenuElements] Stopped observing events")
|
||||
// Clear cache when stopping observation
|
||||
lastEventTime.removeAll()
|
||||
}
|
||||
|
||||
private func limitCacheSize() {
|
||||
if lastEventTime.count > maxCacheSize {
|
||||
// Remove oldest entries if cache is too large
|
||||
let sortedKeys = lastEventTime.sorted(by: { $0.value < $1.value })
|
||||
for i in 0..<(lastEventTime.count - maxCacheSize) {
|
||||
lastEventTime.removeValue(forKey: sortedKeys[i].key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func canEmitEvent(named eventName: String) -> Bool {
|
||||
let now = Date().timeIntervalSince1970
|
||||
|
||||
if let lastTime = lastEventTime[eventName], now - lastTime < throttleInterval {
|
||||
NSLog("[MenuElements] Throttling event: \(eventName)")
|
||||
return false
|
||||
}
|
||||
|
||||
lastEventTime[eventName] = now
|
||||
limitCacheSize() // Keep cache size in check
|
||||
|
||||
let canEmit = hasListeners && bridge != nil
|
||||
if (!canEmit) {
|
||||
NSLog("[MenuElements] Cannot emit event: \(eventName), hasListeners: \(hasListeners), bridge: \(bridge != nil ? "available" : "unavailable")")
|
||||
}
|
||||
|
||||
return canEmit
|
||||
}
|
||||
|
||||
private func safelyEmitEvent(withName name: String) {
|
||||
guard canEmitEvent(named: name) else { return }
|
||||
|
||||
NSLog("[MenuElements] Emitting event: \(name)")
|
||||
|
||||
// Use weak self to avoid retain cycles
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self, self.bridge != nil, self.hasListeners else {
|
||||
NSLog("[MenuElements] Failed to emit event: \(name) - bridge or listeners not available")
|
||||
return
|
||||
}
|
||||
self.sendEvent(withName: name, body: nil)
|
||||
NSLog("[MenuElements] Event sent: \(name)")
|
||||
}
|
||||
}
|
||||
|
||||
func removeAllListeners() {
|
||||
NSLog("[MenuElements] Removing all listeners")
|
||||
// Clean up resources
|
||||
lastEventTime.removeAll()
|
||||
}
|
||||
|
||||
@objc func openSettings() {
|
||||
NSLog("[MenuElements] openSettings method called")
|
||||
safelyEmitEvent(withName: "openSettings")
|
||||
}
|
||||
|
||||
@objc func addWalletMenuAction() {
|
||||
NSLog("[MenuElements] addWalletMenuAction method called")
|
||||
safelyEmitEvent(withName: "addWalletMenuAction")
|
||||
}
|
||||
|
||||
@objc func importWalletMenuAction() {
|
||||
NSLog("[MenuElements] importWalletMenuAction method called")
|
||||
safelyEmitEvent(withName: "importWalletMenuAction")
|
||||
}
|
||||
|
||||
@objc func reloadTransactionsMenuAction() {
|
||||
safelyEmitEvent(withName: "reloadTransactionsMenuAction")
|
||||
}
|
||||
|
||||
override func invalidate() {
|
||||
NSLog("[MenuElements] Module invalidated")
|
||||
if MenuElementsEmitter.sharedInstance === self {
|
||||
MenuElementsEmitter.sharedInstance = nil
|
||||
}
|
||||
removeAllListeners()
|
||||
super.invalidate()
|
||||
}
|
||||
}
|
|
@ -6,8 +6,8 @@ def node_require(script)
|
|||
{paths: [process.argv[1]]},
|
||||
)", __dir__]).strip
|
||||
end
|
||||
|
||||
min_ios_version_supported = '14.0'
|
||||
ENV['RCT_NEW_ARCH_ENABLED'] = '0'
|
||||
min_ios_version_supported = '15.1'
|
||||
node_require('react-native/scripts/react_native_pods.rb')
|
||||
node_require('react-native-permissions/scripts/setup.rb')
|
||||
|
||||
|
@ -54,7 +54,7 @@ post_install do |installer|
|
|||
)
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.1'
|
||||
|
||||
if ['React-Core-AccessibilityResources'].include? target.name
|
||||
config.build_settings['CODE_SIGN_STYLE'] = "Manual"
|
||||
|
|
675
ios/Podfile.lock
675
ios/Podfile.lock
File diff suppressed because it is too large
Load diff
|
@ -30,8 +30,8 @@ class MarketAPI {
|
|||
return "https://www.bnr.ro/nbrfxrates.xml"
|
||||
case "Kraken":
|
||||
return "https://api.kraken.com/0/public/Ticker?pair=XXBTZ\(endPointKey.uppercased())"
|
||||
default:
|
||||
return "https://api.coindesk.com/v1/bpi/currentprice/\(endPointKey).json"
|
||||
default: // CoinDesk
|
||||
return "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=\(endPointKey)"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,8 +131,14 @@ class MarketAPI {
|
|||
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw CurrencyError(errorDescription: "Unsupported data source \(source)")
|
||||
default: // CoinDesk
|
||||
if let rateDouble = json[endPointKey] as? Double {
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
latestRateDataStore = WidgetDataStore(rate: String(rateDouble), lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
return latestRateDataStore
|
||||
} else {
|
||||
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"storage_is_encrypted": "وحدة التخزين مشفرة. أنت بحاجة إلى كلمة المرور لفك تشفيرها",
|
||||
"yes": "نعم",
|
||||
"no": "لا",
|
||||
"save": "حفظ",
|
||||
"seed": "عبارة الاسترداد",
|
||||
"success": "نجاح",
|
||||
"wallet_key": "مفتاح المحفظة",
|
||||
|
@ -210,13 +209,8 @@
|
|||
"set_electrum_server_as_default": "هل تريد تعيين {server} كخادم Electrum الافتراضي؟",
|
||||
"electrum_settings_server": "خادم Electrum",
|
||||
"electrum_status": "الحالة",
|
||||
"electrum_clear_alert_title": "محو السجل؟",
|
||||
"electrum_clear_alert_message": "هل تريد مسح سجل خوادم Electrum؟",
|
||||
"electrum_clear_alert_cancel": "الغاء",
|
||||
"electrum_clear_alert_ok": "موافق",
|
||||
"electrum_reset": "إعادة تعيين إلى الافتراضي",
|
||||
"electrum_unable_to_connect": "تعذر الاتصال بـ {server}.",
|
||||
"electrum_reset_to_default": "هل أنت متأكد من رغبتك في إعادة تعيين إعدادات Electrum إلى الإعدادات الافتراضية؟",
|
||||
"electrum_reset": "إعادة تعيين إلى الافتراضي",
|
||||
"encrypt_decrypt": "فك تشفير وحدة التخزين",
|
||||
"encrypt_decrypt_q": "هل أنت متأكد أنك تريد فك تشفير وحدة التخزين الخاصة بك؟ سيسمح إجراء ذلك بالوصول إلى محافظك دون كلمة مرور.",
|
||||
"encrypt_enc_and_pass": "مشفرة ومحمية بكلمة مرور",
|
||||
|
@ -337,7 +331,6 @@
|
|||
"details_export_history": "تصدير السجل ل ملف CSV",
|
||||
"details_master_fingerprint": "البصمة الرئيسية",
|
||||
"details_multisig_type": "متعدد التواقيع",
|
||||
"details_no_cancel": "لا، إلغاء",
|
||||
"details_show_xpub": "إظهار عنوان XPUB للمحفظة",
|
||||
"details_show_addresses": "عرض العناوين",
|
||||
"details_title": "المحفظة",
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"storage_is_encrypted": "Ваша сховішча зашыфравана. Для расшыфроўкі патрабуецца пароль.",
|
||||
"yes": "Так",
|
||||
"no": "Не",
|
||||
"save": "Захаваць",
|
||||
"seed": "Семя",
|
||||
"success": "Посьпех",
|
||||
"wallet_key": "Ключ ад кашалька"
|
||||
|
@ -44,7 +43,6 @@
|
|||
"create_to": "Да"
|
||||
},
|
||||
"settings": {
|
||||
"electrum_clear_alert_cancel": "Адмяніць",
|
||||
"save": "Захаваць"
|
||||
},
|
||||
"wallets": {
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
"storage_is_encrypted": "Вашият портфейл е криптиран. Необходима е парола за декриптиране",
|
||||
"yes": "Да",
|
||||
"no": "Не",
|
||||
"save": "Запази",
|
||||
"seed": "Сиид",
|
||||
"success": "Успех",
|
||||
"wallet_key": "Парола на портфейла"
|
||||
|
@ -168,13 +167,8 @@
|
|||
"electrum_saved": "Промените бяха запазени успешно. Моля, рестартирайте Блу Уолет за да видите промените.",
|
||||
"set_electrum_server_as_default": "Задайте {server} като Електрум сървър по подразбиране? ",
|
||||
"electrum_status": "Статус",
|
||||
"electrum_clear_alert_title": "Изчисти историята?",
|
||||
"electrum_clear_alert_message": "Искате ли да изтриете електрум сървър историята?",
|
||||
"electrum_clear_alert_cancel": "Отказ",
|
||||
"electrum_clear_alert_ok": "Ок",
|
||||
"electrum_reset": "Начални настройки",
|
||||
"electrum_unable_to_connect": "Не възможно свързване със сървър {server}.",
|
||||
"electrum_reset_to_default": "Сигурни ли сте, че искате да върнете Електрум към първоначалните настройки?",
|
||||
"electrum_reset": "Начални настройки",
|
||||
"encrypt_decrypt": "Декриптирай хранилището",
|
||||
"encrypt_decrypt_q": "Сигурни ли сте, че искате да декриптирате хранилището? Това ще направи портфейлите ви достъпни без парола.",
|
||||
"encrypt_enc_and_pass": "Криптиран и защитен с парола",
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
"storage_is_encrypted": "جاگه زفت کردنی ایسا ریس رزم هڌ. سی گۊشیڌنس وا رزمسه داشته بۊی.",
|
||||
"yes": "هری",
|
||||
"no": "نه",
|
||||
"save": "زفت کردن",
|
||||
"seed": "سید",
|
||||
"success": "سر ٱنجوم گرهڌ",
|
||||
"wallet_key": "کیلیت کیف پیل",
|
||||
|
@ -187,13 +186,8 @@
|
|||
"set_electrum_server_as_default": "{server} سی سرور پؽش فرز الکترام ساموو بۊوه؟",
|
||||
"electrum_settings_server": "سرور الکترام",
|
||||
"electrum_status": "وزیت",
|
||||
"electrum_clear_alert_title": "ویرگار پاک بۊ؟",
|
||||
"electrum_clear_alert_message": "اخۊی ویرگار سرورا الکترام نه پاک کۊنی؟",
|
||||
"electrum_clear_alert_cancel": "لقو",
|
||||
"electrum_clear_alert_ok": "هری",
|
||||
"electrum_reset": "ورگندن به پؽش فرز",
|
||||
"electrum_unable_to_connect": "نا مۉفق منه منپیز به {server}",
|
||||
"electrum_reset_to_default": "الن اخۊی سامووا الکترام نه به هالت پؽش فرز وورنشۊوی کۊنی؟",
|
||||
"electrum_reset": "ورگندن به پؽش فرز",
|
||||
"encrypt_decrypt": "رزم گوشایی جاگه زفت کردنی",
|
||||
"encrypt_title": "امنیت",
|
||||
"encrypt_tstorage": "جاگه زفت کردنی",
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"storage_is_encrypted": "L'informació està xifrada. Es requereix la contrasenya per a desxifrar-la.",
|
||||
"yes": "Si",
|
||||
"no": "No",
|
||||
"save": "Desar",
|
||||
"seed": "Llavor",
|
||||
"success": "Èxit",
|
||||
"wallet_key": "Clau del moneder",
|
||||
|
@ -168,9 +167,6 @@
|
|||
"use_ssl": "Utilitza SSL",
|
||||
"electrum_settings_server": "Servidor Electrum",
|
||||
"electrum_status": "estat",
|
||||
"electrum_clear_alert_title": "Netejar l'historial?",
|
||||
"electrum_clear_alert_cancel": "Cancel·lar",
|
||||
"electrum_clear_alert_ok": "D'acord",
|
||||
"electrum_reset": "Restableix la configuració predeterminada",
|
||||
"encrypt_title": "Seguretat",
|
||||
"encrypt_use": "Utilitza {type}",
|
||||
|
@ -237,7 +233,6 @@
|
|||
"details_delete_wallet": "Eliminar Moneder",
|
||||
"details_export_backup": "Exportar / Guardar",
|
||||
"details_master_fingerprint": "Petjada digital mestre",
|
||||
"details_no_cancel": "No, cancel·lar",
|
||||
"details_show_xpub": "Mostrar wallet XPUB",
|
||||
"details_title": "Detalls del moneder",
|
||||
"wallets": "moneders",
|
||||
|
|
|
@ -10,12 +10,11 @@
|
|||
"never": "Nikdy",
|
||||
"of": "{number} z(e) {total}",
|
||||
"ok": "OK",
|
||||
"customize": "Přizpůsobit",
|
||||
"enter_url": "Zadejte URL",
|
||||
"storage_is_encrypted": "Vaše úložiště je zašifrované. Zadejte heslo k odemčení.",
|
||||
"yes": "Ano",
|
||||
"no": "Ne",
|
||||
"save": "Uložit",
|
||||
"save": "Uložit…",
|
||||
"seed": "Seed",
|
||||
"success": "Úspěch",
|
||||
"wallet_key": "Klíč peněženky",
|
||||
|
@ -28,6 +27,8 @@
|
|||
"enter_amount": "Zadejte částku",
|
||||
"qr_custom_input_button": "Klepněte 10× k zadání vlastního vstupu",
|
||||
"unlock": "Odemknout",
|
||||
"port": "Port",
|
||||
"ssl_port": "SSL port",
|
||||
"suggested": "Doporučené"
|
||||
},
|
||||
"azteco": {
|
||||
|
@ -74,6 +75,7 @@
|
|||
"please_pay": "Zaplaťte prosím",
|
||||
"preimage": "Předobraz",
|
||||
"sats": "sats.",
|
||||
"date_time": "Datum a čas",
|
||||
"wasnt_paid_and_expired": "Tato faktura nebyla zaplacena a její platnost vypršela."
|
||||
},
|
||||
"plausibledeniability": {
|
||||
|
@ -106,7 +108,7 @@
|
|||
"minSats": "Minimální množství je {min} sats",
|
||||
"minSatsFull": "Minimální částka je {min} sats nebo {currency} ",
|
||||
"qrcode_for_the_address": "QR kód pro adresu",
|
||||
"bip47_explanation": "Platební kódy jsou univerzální adresou, která zabraňuje odhalení adres vaší peněženky. Nejsou podporovány všemi službami."
|
||||
"bip47_explanation": "Platební kódy jsou univerzální adresy, které zabraňují prozrazení adres vaší peněženky. Ne všechny služby je však podporují."
|
||||
},
|
||||
"send": {
|
||||
"provided_address_is_invoice": "Zdá se, že tato adresa je určena pro Lightning fakturu. Přejděte prosím do své Lightning peněženky, abyste mohli provést platbu této faktury.",
|
||||
|
@ -134,6 +136,9 @@
|
|||
"details_add_recc_rem_all_alert_description": "Jste si jisti, že chcete odebrat všechny příjemce?",
|
||||
"details_add_rec_rem_all": "Odebrat všechny příjemce",
|
||||
"details_recipients_title": "Příjemci",
|
||||
"details_recipient_title": "Příjemce č. {number} z(e) {total}",
|
||||
"please_complete_recipient_title": "Nekompletní příjemce",
|
||||
"please_complete_recipient_details": "Vyplňte prosím všechny detaily příjemce č. {number} před přidáním nového příjemce.",
|
||||
"details_address": "Adresa",
|
||||
"details_address_field_is_not_valid": "Adresa není správně vyplněna.",
|
||||
"details_adv_fee_bump": "Povolit navýšení poplatku",
|
||||
|
@ -180,6 +185,7 @@
|
|||
"input_total": "Celkem:",
|
||||
"permission_camera_message": "K použití fotoaparátu potřebujeme vaše povolení",
|
||||
"psbt_sign": "Podepsat transakci",
|
||||
"invalid_psbt": "Byla poskytnuta neplatná PSBT.",
|
||||
"open_settings": "Otevřít nastavení",
|
||||
"permission_storage_denied_message": "BlueWallet nemůže tento soubor uložit. Otevřete prosím nastavení zařízení a povolte funkci Oprávnění k ukládání.",
|
||||
"permission_storage_title": "Povolení k přístupu do úložiště",
|
||||
|
@ -190,7 +196,7 @@
|
|||
"outdated_rate": "Kurz byl naposledy aktualizován: {date}",
|
||||
"psbt_tx_open": "Otevřít podepsanou transakci",
|
||||
"psbt_tx_scan": "Skenovat podepsanou transakci",
|
||||
"qr_error_no_qrcode": "Ve vybraném obrázku se nám nepodařilo najít QR kód. Ujistěte se, že obrázek obsahuje pouze QR kód a žádný další obsah, například text nebo tlačítka.",
|
||||
"qr_error_no_qrcode": "Ve vybraném obrázku nebyl nalezen platný QR kód. Zajistěte, aby obrázek obsahoval pouze QR kód a žádný další obsah jako třeba text nebo tlačítka.",
|
||||
"reset_amount": "Vynulovat částku",
|
||||
"reset_amount_confirm": "Chcete částku vynulovat?",
|
||||
"success_done": "Hotovo",
|
||||
|
@ -250,15 +256,11 @@
|
|||
"electrum_status": "Stav",
|
||||
"electrum_preferred_server": "Upřednostňovaný server",
|
||||
"electrum_preferred_server_description": "Zadejte server, který má vaše peněženka používat pro všechny bitcoinové aktivity. Po nastavení bude vaše peněženka používat výhradně tento server ke kontrole zůstatků, odesílání transakcí a načítání síťových dat. Před nastavením se ujistěte, že tomuto serveru důvěřujete.",
|
||||
"electrum_clear_alert_title": "Smazat historii?",
|
||||
"electrum_clear_alert_message": "Chcete vymazat historii serverů Electrum?",
|
||||
"electrum_clear_alert_cancel": "Zrušit",
|
||||
"electrum_clear_alert_ok": "Ok",
|
||||
"electrum_reset": "Obnovit do výchozího nastavení",
|
||||
"electrum_unable_to_connect": "Nelze se připojit k {server}.",
|
||||
"electrum_history": "Historie",
|
||||
"electrum_reset_to_default": "Opravdu chcete obnovit nastavení Electrum na výchozí hodnoty?",
|
||||
"electrum_clear": "Vymazat historii",
|
||||
"electrum_reset_to_default": "Toto nastavení nechá aplikaci BlueWallet náhodně vybrat server ze seznamu navrhovaných.",
|
||||
"electrum_reset": "Obnovit do výchozího nastavení",
|
||||
"electrum_reset_to_default_and_clear_history": "Obnovit výchozí nastavení a vymazat historii",
|
||||
"encrypt_decrypt": "Dešifrovat úložiště",
|
||||
"encrypt_decrypt_q": "Opravdu chcete dešifrovat úložiště? To umožní přistupovat k vašim peněženkám bez hesla.",
|
||||
"encrypt_enc_and_pass": "Šifrovat a chránit heslem",
|
||||
|
@ -272,6 +274,8 @@
|
|||
"encrypt_title": "Zabezpečení",
|
||||
"encrypt_tstorage": "Úložiště",
|
||||
"encrypt_use": "Použít {type}",
|
||||
"set_as_preferred": "Nastavit jako preferovaný",
|
||||
"set_as_preferred_electrum": "Nastavení {host}:{port} jako upřednostňovaného serveru zakáže náhodné připojování k navrhovanému serveru.",
|
||||
"encrypted_feature_disabled": "Tato funkce nemůže být použita, pokud je povoleno zašifrované úložiště.",
|
||||
"encrypt_use_expl": "{type} bude použit k potvrzení vaší identity před provedením transakce, odemknutím, exportem nebo smazáním peněženky. {type} nebude použit k odemknutí zašifrovaného úložiště.",
|
||||
"biometrics_fail": "Pokud {type} není povolen, nebo selže při odemykání, můžete jako alternativu použít přístupový kód vašeho zařízení.",
|
||||
|
@ -291,6 +295,7 @@
|
|||
"network": "Síť",
|
||||
"network_broadcast": "Odeslat transakci",
|
||||
"network_electrum": "Electrum server",
|
||||
"electrum_suggested_description": "Pokud nebude nastaven preferovaný server, bude náhodně vybrán navrhovaný server.",
|
||||
"not_a_valid_uri": "Neplatná URI",
|
||||
"notifications": "Oznámení",
|
||||
"open_link_in_explorer": "Otevřít odkaz v průzkumníku",
|
||||
|
@ -402,7 +407,6 @@
|
|||
"add_wallet_name": "Název peněženky",
|
||||
"add_wallet_type": "Typ",
|
||||
"add_wallet_seed_length": "Délka seedu",
|
||||
"add_wallet_seed_length_message": "Zvolte délku seed fráze, kterou chcete použít pro tuto peněženku.",
|
||||
"add_wallet_seed_length_12": "12 slov",
|
||||
"add_wallet_seed_length_24": "24 slov",
|
||||
"clipboard_bitcoin": "Ve schránce máte bitcoinovou adresu. Chcete ji použít pro transakci?",
|
||||
|
@ -422,7 +426,6 @@
|
|||
"details_export_history": "Exportovat historii do CSV",
|
||||
"details_master_fingerprint": "Hlavní otisk",
|
||||
"details_multisig_type": "multisig",
|
||||
"details_no_cancel": "Ne, zrušit",
|
||||
"details_show_xpub": "Zobrazit XPUB peněženky",
|
||||
"details_show_addresses": "Zobrazit adresy",
|
||||
"details_title": "Peněženka",
|
||||
|
@ -493,7 +496,9 @@
|
|||
"identity_pubkey": "Identity Pubkey",
|
||||
"xpub_title": "XPUB peněženky",
|
||||
"manage_wallets_search_placeholder": "Prohledat peněženky, poznámky",
|
||||
"more_info": "Další informace"
|
||||
"more_info": "Další informace",
|
||||
"details_delete_wallet_error_message": "Došlo k problému během potvrzování, zda byla tato peněženka odstraněna z oznámení. Toto může být způsobeno problémem se sítí nebo kvůli špatnému připojení. Pokud budete pokračovat, je možné, že i nadále budete dostávat oznámení o transakcích souvisejících s touto peněženkou, přestože už bude odstraněna.",
|
||||
"details_delete_anyway": "Přesto odstranit"
|
||||
},
|
||||
"total_balance_view": {
|
||||
"display_in_bitcoin": "Zobrazit v bitcoinech",
|
||||
|
@ -508,6 +513,10 @@
|
|||
"default_label": "Vícepodpisové Úložiště",
|
||||
"multisig_vault_explain": "Nejlepší zabezpečení pro velké částky",
|
||||
"provide_signature": "Poskytnout podpis",
|
||||
"provide_signature_details": "K podpisu této transakce použijte své zařízení a peněženku, ve které je klíč umístěn",
|
||||
"provide_signature_details_bluewallet": "V aplikaci BlueWallet přejděte do nabídky Odeslat a vyberte",
|
||||
"provide_signature_next_steps": "Naskenujte nebo importujte podepsanou transakci",
|
||||
"provide_signature_next_steps_details": "Poté, co vaše peněženka úspěšně podepsala transakci, naskenujte uvedený QR kód, nebo importujte doprovodný soubor, a poté zkontrolujte veškeré podrobnosti transakce před tím, než ji odešlete.",
|
||||
"vault_key": "{number}. klíč Úložiště",
|
||||
"required_keys_out_of_total": "Požadovaných klíčů z celkového počtu",
|
||||
"fee": "Poplatek: {number}",
|
||||
|
@ -654,6 +663,8 @@
|
|||
"bip47": {
|
||||
"payment_code": "Platební kód",
|
||||
"contacts": "Kontakty",
|
||||
"bip47_explain": "Znovu použitelný a sdílitelný kód",
|
||||
"bip47_explain_subtitle": "BIP47",
|
||||
"purpose": "Opakovaně použitelný kód, který je možné sdílet (BIP47)",
|
||||
"pay_this_contact": "Zaplatit tomuto kontaktu",
|
||||
"rename_contact": "Přejmenovat kontakt",
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
"storage_is_encrypted": "Mae'r storfa wedi encryptio. Mae angen Cyfrinair i'w ddad-gryptio. ",
|
||||
"yes": "Ie",
|
||||
"no": "Na",
|
||||
"save": "Safio",
|
||||
"seed": "Hadyn",
|
||||
"success": "Llwyddiant",
|
||||
"wallet_key": "Allwedd waled"
|
||||
|
@ -109,8 +108,6 @@
|
|||
"default_wallets": "Gweld Pob Waled",
|
||||
"electrum_connected": "Wedi Cysylltu",
|
||||
"electrum_connected_not": "Heb Gysylltu",
|
||||
"electrum_clear_alert_cancel": "Canslo",
|
||||
"electrum_clear_alert_ok": "Iawn",
|
||||
"encrypt_decrypt": "Dadgryptio'r Storfa",
|
||||
"encrypt_title": "Diogelwch",
|
||||
"encrypt_tstorage": "Storfa",
|
||||
|
@ -163,7 +160,6 @@
|
|||
"details_connected_to": "Wedi cysylltu efo",
|
||||
"details_delete": "Gwaredu",
|
||||
"details_delete_wallet": "Gwaredu Waled",
|
||||
"details_no_cancel": "Na, canslo",
|
||||
"details_show_addresses": "Dangos cyfeiriadau",
|
||||
"wallets": "Waledi",
|
||||
"details_type": "Math",
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
"never": "aldrig",
|
||||
"ok": "OK",
|
||||
"storage_is_encrypted": "Lageret er krypteret. Indtast adgangskode for at dekryptere",
|
||||
"save": "save",
|
||||
"success": "Succes"
|
||||
},
|
||||
"azteco": {
|
||||
|
@ -60,7 +59,6 @@
|
|||
"settings": {
|
||||
"about": "Andet",
|
||||
"currency": "Valuta",
|
||||
"electrum_clear_alert_cancel": "Annuller",
|
||||
"header": "indstillinger",
|
||||
"language": "Sprog",
|
||||
"lightning_settings": "Lightning settings",
|
||||
|
@ -87,7 +85,6 @@
|
|||
"details_are_you_sure": "Er du sikker?",
|
||||
"details_delete": "Slet",
|
||||
"details_export_backup": "Eksporter / backup",
|
||||
"details_no_cancel": "Nej, annuller",
|
||||
"details_show_xpub": "Vis wallet XPUB",
|
||||
"details_yes_delete": "Ja, slet",
|
||||
"export_title": "wallet eksport",
|
||||
|
|
|
@ -10,12 +10,11 @@
|
|||
"never": "nie",
|
||||
"of": "{number} von {total}",
|
||||
"ok": "OK",
|
||||
"customize": "Anpassen",
|
||||
"enter_url": "URL eingeben",
|
||||
"storage_is_encrypted": "Zum Entschlüsseln des Speichers das Passwort eingeben.",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"save": "Speichern",
|
||||
"save": "Speichern...",
|
||||
"seed": "Seed",
|
||||
"success": "Erfolg",
|
||||
"wallet_key": "Wallet Schlüssel",
|
||||
|
@ -255,8 +254,9 @@
|
|||
"electrum_preferred_server_description": "Den Server eingeben, der die Wallet für alle Bitcoin-Aktivitäten verwenden soll. Sobald festgelegt, wird die Wallet um Salden abzufragen, Transaktionen zu senden und Netzwerkdaten abzurufen ausschliesslich diesen Server verwenden. Prüfen sie vorher, dass der Server vertrauenswürdig ist.",
|
||||
"electrum_unable_to_connect": "Verbindung zu {server} kann nicht hergestellt werden.",
|
||||
"electrum_history": "Historie",
|
||||
"electrum_reset_to_default": "BlueWallet wählt zufällig einen Server aus der Vorschlagsliste oder Historie aus. Der Serververlauf bleibt dadurch unverändert.",
|
||||
"electrum_reset_to_default": "Dies lässt BlueWallet zufällig einen Server aus der Liste der Server auswählen.",
|
||||
"electrum_reset": "Zurücksetzten",
|
||||
"electrum_reset_to_default_and_clear_history": "Auf die Standardeinstellungen zurücksetzen und den Verlauf löschen.",
|
||||
"encrypt_decrypt": "Speicher entschlüsseln",
|
||||
"encrypt_decrypt_q": "Die Speicherverschlüsselung wirklich aufheben? Hiermit werden die Wallet ohne Passwortschutz direkt benutzbar. ",
|
||||
"encrypt_enc_and_pass": "Verschlüsselt und passwortgeschützt",
|
||||
|
@ -403,7 +403,6 @@
|
|||
"add_wallet_name": "Wallet Name",
|
||||
"add_wallet_type": "Typ",
|
||||
"add_wallet_seed_length": "Seedlänge",
|
||||
"add_wallet_seed_length_message": "Länge der Seed-Phrase für diese Wallet wählen.",
|
||||
"add_wallet_seed_length_12": "12 Wörter",
|
||||
"add_wallet_seed_length_24": "24 Wörter",
|
||||
"clipboard_bitcoin": "In der Zwischenablage ist eine Bitcoin Adresse. Soll diese für eine Transaktion verwendet werden?",
|
||||
|
@ -423,7 +422,6 @@
|
|||
"details_export_history": "Verlauf als CSV exportieren",
|
||||
"details_master_fingerprint": "Fingerabdruckkennung",
|
||||
"details_multisig_type": "Mehrfachsignatur",
|
||||
"details_no_cancel": "Nein, abbrechnen",
|
||||
"details_show_xpub": "Wallet xPub zeigen",
|
||||
"details_show_addresses": "Adressen anzeigen",
|
||||
"details_title": "Wallet",
|
||||
|
@ -494,7 +492,9 @@
|
|||
"identity_pubkey": "Pubkey-Identität",
|
||||
"xpub_title": "Wallet xPub",
|
||||
"manage_wallets_search_placeholder": "Suche in Wallets, Notizen",
|
||||
"more_info": "Mehr Infos"
|
||||
"more_info": "Mehr Infos",
|
||||
"details_delete_wallet_error_message": "Bei der Bestätigung, ob diese Wallet aus den Benachrichtigungen entfernt wurde, trat ein Fehler auf – möglicherweise wegen Netzwerkproblemen oder schlechter Verbindung. Falls du fortfährst, erhältst du möglicherweise weiterhin Benachrichtigungen für Transaktionen dieser Wallet, auch nachdem sie gelöscht wurde.",
|
||||
"details_delete_anyway": "Trotzdem löschen"
|
||||
},
|
||||
"total_balance_view": {
|
||||
"display_in_bitcoin": "In bitcoin anzeigen",
|
||||
|
@ -670,7 +670,7 @@
|
|||
"notification_tx_unconfirmed": "Benachrichtigungstransaktion noch unbestätigt. Bitte warten",
|
||||
"failed_create_notif_tx": "On-Chain Transaktion konnte nicht in erstellt werden",
|
||||
"onchain_tx_needed": "On-Chain Transaktion benötigt.",
|
||||
"notif_tx_sent" : "Benachrichtigungstransaktion ist gesendet. Auf Bestätigung warten.",
|
||||
"notif_tx_sent": "Benachrichtigungstransaktion ist gesendet. Auf Bestätigung warten.",
|
||||
"notif_tx": "Benachrichtigungstransaktion",
|
||||
"not_found": "Zahlungscode nicht gefunden"
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
"storage_is_encrypted": "Το αρχείο σου είναι κρυπτογραφημένο. Χρειάζεται ένας κωδικός για να αποκρυπτογραφηθεί.",
|
||||
"yes": "Ναι",
|
||||
"no": "Όχι",
|
||||
"save": "Αποθήκευση",
|
||||
"success": "Επιτυχία",
|
||||
"close": "Κλείσιμο",
|
||||
"refresh": "Ανανέωση",
|
||||
|
@ -168,11 +167,8 @@
|
|||
"use_ssl": "Χρήση SSL",
|
||||
"electrum_settings_server": "Διακομιστής Electrum",
|
||||
"electrum_status": "Κατάσταση",
|
||||
"electrum_clear_alert_title": "Καθαρισμός ιστορικού;",
|
||||
"electrum_clear_alert_cancel": "Ακύρωση",
|
||||
"electrum_clear_alert_ok": "Εντάξει",
|
||||
"electrum_reset": "Επαναφορά προεπιλογής",
|
||||
"electrum_unable_to_connect": "Αδυναμία σύνδεσης στον {server}.",
|
||||
"electrum_reset": "Επαναφορά προεπιλογής",
|
||||
"encrypt_decrypt": "Αποκρυπτογράφηση αποθηκευτικού χώρου",
|
||||
"encrypt_title": "Ασφάλεια",
|
||||
"encrypt_use": "Χρήση {type}",
|
||||
|
@ -243,7 +239,6 @@
|
|||
"details_delete": "Διαγραφή",
|
||||
"details_delete_wallet": "Διαγραφή πορτοφολιού",
|
||||
"details_export_backup": "Εξήγαγε / δημιούργησε αντίγραφο ασφαλείας",
|
||||
"details_no_cancel": "Όχι, ακύρωσε",
|
||||
"details_show_xpub": "Προβολή XPUB του πορτοφολιού",
|
||||
"details_show_addresses": "Εμφάνιση διευθύνσεων",
|
||||
"details_title": "Πορτοφόλι",
|
||||
|
|
21
loc/en.json
21
loc/en.json
|
@ -14,7 +14,7 @@
|
|||
"storage_is_encrypted": "Your storage is encrypted. Password is required to decrypt it.",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"save": "Save",
|
||||
"save": "Save...",
|
||||
"seed": "Seed",
|
||||
"success": "Success",
|
||||
"wallet_key": "Wallet key",
|
||||
|
@ -136,6 +136,9 @@
|
|||
"details_add_recc_rem_all_alert_description": "Are you sure you want to remove all recipients?",
|
||||
"details_add_rec_rem_all": "Remove All Recipients",
|
||||
"details_recipients_title": "Recipients",
|
||||
"details_recipient_title": "Recipient #{number} of #{total}",
|
||||
"please_complete_recipient_title": "Incomplete Recipient",
|
||||
"please_complete_recipient_details": "Please complete the details of recipient #{number} before adding a new recipient.",
|
||||
"details_address": "Address",
|
||||
"details_address_field_is_not_valid": "The address is not valid.",
|
||||
"details_adv_fee_bump": "Allow Fee Bump",
|
||||
|
@ -182,6 +185,7 @@
|
|||
"input_total": "Total:",
|
||||
"permission_camera_message": "We need your permission to use your camera.",
|
||||
"psbt_sign": "Sign a transaction",
|
||||
"invalid_psbt": "Invalid PSBT provided.",
|
||||
"open_settings": "Open Settings",
|
||||
"permission_storage_denied_message": "BlueWallet is unable to save this file. Please open your device settings and enable Storage Permission.",
|
||||
"permission_storage_title": "Storage Access Permission",
|
||||
|
@ -239,6 +243,7 @@
|
|||
"electrum_connected": "Connected",
|
||||
"electrum_connected_not": "Not Connected",
|
||||
"electrum_error_connect": "Cannot connect to the provided Electrum server",
|
||||
"electrum_error_connect_tor": "Cannot connect to the provided Electrum server. Please make sure Orbot app is connected and try again.",
|
||||
"lndhub_uri": "E.g., {example}",
|
||||
"electrum_host": "E.g., {example}",
|
||||
"electrum_offline_mode": "Offline Mode",
|
||||
|
@ -254,8 +259,9 @@
|
|||
"electrum_preferred_server_description": "Enter the server you want your wallet to use for all Bitcoin activities. Once set, your wallet will exclusively use this server to check balances, send transactions, and fetch network data. Ensure you trust this server before setting it.",
|
||||
"electrum_unable_to_connect": "Unable to connect to {server}.",
|
||||
"electrum_history": "History",
|
||||
"electrum_reset_to_default": "This will let BlueWallet randomly choose a server from the suggested list and history. Your server history will remain unchanged.",
|
||||
"electrum_reset_to_default": "This will let BlueWallet randomly choose a server from the server list.",
|
||||
"electrum_reset": "Reset to default",
|
||||
"electrum_reset_to_default_and_clear_history": "Reset to default and clear history",
|
||||
"encrypt_decrypt": "Decrypt Storage",
|
||||
"encrypt_decrypt_q": "Are you sure you want to decrypt your storage? This will allow your wallets to be accessed without a password.",
|
||||
"encrypt_enc_and_pass": "Encrypted and Password Protected",
|
||||
|
@ -284,6 +290,7 @@
|
|||
"language_isRTL": "Restarting BlueWallet is required for the language orientation to take effect.",
|
||||
"license": "License",
|
||||
"lightning_error_lndhub_uri": "Invalid LNDhub URI",
|
||||
"lightning_error_lndhub_uri_tor": "Invalid LNDhub URI. Please make sure Orbot app is connected and try again.",
|
||||
"lightning_saved": "Your changes have been saved successfully.",
|
||||
"lightning_settings": "Lightning Settings",
|
||||
"lightning_settings_explain": "To connect to your own LND node, please install LNDhub and put its URL here in settings. Please note that only wallets created after saving changes will connect to the specified LNDhub.",
|
||||
|
@ -402,7 +409,6 @@
|
|||
"add_wallet_name": "Name",
|
||||
"add_wallet_type": "Type",
|
||||
"add_wallet_seed_length": "Seed Length",
|
||||
"add_wallet_seed_length_message": "Seed phrase length",
|
||||
"add_wallet_seed_length_12": "12 words",
|
||||
"add_wallet_seed_length_24": "24 words",
|
||||
"clipboard_bitcoin": "You have a Bitcoin address on your clipboard. Would you like to use it for a transaction?",
|
||||
|
@ -422,7 +428,6 @@
|
|||
"details_export_history": "Export History to CSV",
|
||||
"details_master_fingerprint": "Master Fingerprint",
|
||||
"details_multisig_type": "multisig",
|
||||
"details_no_cancel": "No, cancel",
|
||||
"details_show_xpub": "Show Wallet XPUB",
|
||||
"details_show_addresses": "Show addresses",
|
||||
"details_title": "Wallet",
|
||||
|
@ -493,7 +498,9 @@
|
|||
"identity_pubkey": "Identity Pubkey",
|
||||
"xpub_title": "Wallet XPUB",
|
||||
"manage_wallets_search_placeholder": "Search wallets, memos",
|
||||
"more_info": "More Info"
|
||||
"more_info": "More Info",
|
||||
"details_delete_wallet_error_message": "There was an issue confirming if this wallet was removed from notifications—this could be due to a network issue or poor connection. If you continue, you might still receive notifications for transactions related to this wallet, even after it is deleted.",
|
||||
"details_delete_anyway": "Delete anyway"
|
||||
},
|
||||
"total_balance_view": {
|
||||
"display_in_bitcoin": "Display in Bitcoin",
|
||||
|
@ -508,6 +515,10 @@
|
|||
"default_label": "Multisig Vault",
|
||||
"multisig_vault_explain": "Best security for large amounts",
|
||||
"provide_signature": "Provide signature",
|
||||
"provide_signature_details": "Use your device and wallet where the key resides to sign this transaction",
|
||||
"provide_signature_details_bluewallet": "In BlueWallet, go to the Send screen menu and select ",
|
||||
"provide_signature_next_steps": "Scan or Import Signed Transaction",
|
||||
"provide_signature_next_steps_details": "Once your wallet has successfully signed the transaction, scan the provided QR code or import the accompanying file, and then review all the transaction details before broadcasting it.",
|
||||
"vault_key": "Vault Key {number}",
|
||||
"required_keys_out_of_total": "Required keys out of the total",
|
||||
"fee": "Fee: {number}",
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"storage_is_encrypted": "Tu almacenamiento está cifrado. Se requiere la contraseña para descifrarlo.",
|
||||
"yes": "Sí",
|
||||
"no": "No",
|
||||
"save": "Guardar",
|
||||
"seed": "Semilla",
|
||||
"success": "Completado",
|
||||
"wallet_key": "Llave de la cartera"
|
||||
|
@ -201,13 +200,8 @@
|
|||
"set_electrum_server_as_default": "¿Establecer {server} como servidor Electrum por defecto?",
|
||||
"electrum_settings_server": "Servidor Electrum",
|
||||
"electrum_status": "Estado",
|
||||
"electrum_clear_alert_title": "¿Limpiar historial?",
|
||||
"electrum_clear_alert_message": "¿Quieres eliminar el historial de los servidores de Electrum?",
|
||||
"electrum_clear_alert_cancel": "Cancelar",
|
||||
"electrum_clear_alert_ok": "Ok",
|
||||
"electrum_reset": "Restablecer valores predeterminados",
|
||||
"electrum_unable_to_connect": "Imposible conectar a {server}. ",
|
||||
"electrum_reset_to_default": "¿Estás seguro de querer reiniciar sus ajustes de Electrum por defecto? ",
|
||||
"electrum_reset": "Restablecer valores predeterminados",
|
||||
"encrypt_decrypt": "Desencriptar almacenamiento",
|
||||
"encrypt_decrypt_q": "¿Seguro que quieres desencriptar tu almacenamiento? Al hacerlo, se podrá acceder a tus carteras sin contraseña.",
|
||||
"encrypt_enc_and_pass": "Encriptado y protegido mediante contraseña",
|
||||
|
@ -327,7 +321,6 @@
|
|||
"details_export_history": "Exportar el historial a CSV",
|
||||
"details_master_fingerprint": "Huella dactilar maestra",
|
||||
"details_multisig_type": "multifirma",
|
||||
"details_no_cancel": "No, cancelar",
|
||||
"details_show_xpub": "Mostrar el XPUB de la cartera",
|
||||
"details_show_addresses": "Mostrar dirección",
|
||||
"details_title": "Cartera",
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
"never": "Nunca",
|
||||
"of": "{number} de {total}",
|
||||
"ok": "OK",
|
||||
"customize": "Personalizar",
|
||||
"enter_url": "Introducir URL",
|
||||
"storage_is_encrypted": "Tu almacenamiento está encriptado. Se requiere contraseña para descifrarlo.",
|
||||
"yes": "Sí",
|
||||
|
@ -137,6 +136,9 @@
|
|||
"details_add_recc_rem_all_alert_description": "¿Estás seguro de que quieres eliminar todos los destinatarios?",
|
||||
"details_add_rec_rem_all": "Eliminar todos los destinatarios",
|
||||
"details_recipients_title": "Destinatarios",
|
||||
"details_recipient_title": "Destinatario #{number} de #{total}",
|
||||
"please_complete_recipient_title": "Destinatario incompleto",
|
||||
"please_complete_recipient_details": "Completa los detalles del destinatario #{number} antes de agregar un nuevo destinatario.",
|
||||
"details_address": "Dirección",
|
||||
"details_address_field_is_not_valid": "La dirección no es válida.",
|
||||
"details_adv_fee_bump": "Permitir aumento de tarifas",
|
||||
|
@ -183,6 +185,7 @@
|
|||
"input_total": "Total:",
|
||||
"permission_camera_message": "Necesitamos tu permiso para usar tu cámara",
|
||||
"psbt_sign": "Firmar una transacción",
|
||||
"invalid_psbt": "PSBT proporcionado no válido.",
|
||||
"open_settings": "Abrir configuraciones",
|
||||
"permission_storage_denied_message": "BlueWallet no puede guardar este archivo. Por favor, abre la configuración de tu dispositivo y activa el permiso de almacenamiento.",
|
||||
"permission_storage_title": "Permiso de acceso de almacenamiento",
|
||||
|
@ -240,6 +243,7 @@
|
|||
"electrum_connected": "Conectado",
|
||||
"electrum_connected_not": "No Conectado",
|
||||
"electrum_error_connect": "No se puede conectar al servidor Electrum proporcionado",
|
||||
"electrum_error_connect_tor": "No se puede conectar al servidor Electrum proporcionado. Asegúrate de que la aplicación Orbot esté conectada y vuelve a intentarlo.",
|
||||
"lndhub_uri": "Ej.: {example}",
|
||||
"electrum_host": "Ej.: {example}",
|
||||
"electrum_offline_mode": "Modo offline",
|
||||
|
@ -255,8 +259,9 @@
|
|||
"electrum_preferred_server_description": "Introduce el servidor que deseas que tu billetera utilice para todas las actividades de Bitcoin. Una vez configurado, tu billetera utilizará exclusivamente este servidor para comprobar saldos, enviar transacciones y obtener datos de la red. Asegúrate de que confías en este servidor antes de configurarlo.",
|
||||
"electrum_unable_to_connect": "No se puede conectar al {server}.",
|
||||
"electrum_history": "Historial",
|
||||
"electrum_reset_to_default": "Esto permitirá que BlueWallet elija aleatoriamente un servidor de la lista sugerida y del historial. El historial de tu servidor permanecerá sin cambios",
|
||||
"electrum_reset_to_default": "Esto permitirá que BlueWallet elija aleatoriamente un servidor de la lista de servidores.",
|
||||
"electrum_reset": "Restablecer a predeterminado",
|
||||
"electrum_reset_to_default_and_clear_history": "Restablecer valores predeterminados y borrar historial",
|
||||
"encrypt_decrypt": "Descifrar Almacenamiento",
|
||||
"encrypt_decrypt_q": "¿Estás seguro de que deseas descifrar tu almacenamiento? Esto permitirá acceder a tus billeteras sin una contraseña.",
|
||||
"encrypt_enc_and_pass": "Encriptado y protegido con contraseña",
|
||||
|
@ -285,6 +290,7 @@
|
|||
"language_isRTL": "Es necesario reiniciar BlueWallet para que la orientación del idioma surta efecto.",
|
||||
"license": "Licencia",
|
||||
"lightning_error_lndhub_uri": "URI de LNDhub no válido",
|
||||
"lightning_error_lndhub_uri_tor": "La URL de LNDhub no es válida. Asegúrate de que la aplicación Orbot esté conectada y vuelve a intentarlo.",
|
||||
"lightning_saved": "Tus cambios han sido guardados correctamente.",
|
||||
"lightning_settings": "Configuración de Lightning",
|
||||
"lightning_settings_explain": "Para conectarte a tu propio nodo LND, instala LNDhub y coloca su URL aquí en la configuración. Ten en cuenta que solo las billeteras creadas después de guardar los cambios se conectarán al LNDhub especificado.",
|
||||
|
@ -403,7 +409,6 @@
|
|||
"add_wallet_name": "Nombre",
|
||||
"add_wallet_type": "Tipo",
|
||||
"add_wallet_seed_length": "Longitud de la semilla",
|
||||
"add_wallet_seed_length_message": "Elige la longitud de la frase semilla que deseas utilizar para esta billetera.",
|
||||
"add_wallet_seed_length_12": "12 palabras",
|
||||
"add_wallet_seed_length_24": "24 palabras",
|
||||
"clipboard_bitcoin": "Tienes una dirección de Bitcoin en tu portapapeles. ¿te gustaría usarlo para una transacción?",
|
||||
|
@ -423,7 +428,6 @@
|
|||
"details_export_history": "Exportar historial a CSV",
|
||||
"details_master_fingerprint": "Huella Digital Maestra",
|
||||
"details_multisig_type": "multifirma",
|
||||
"details_no_cancel": "No, cancelar",
|
||||
"details_show_xpub": "Mostrar el XPUB de la Billetera",
|
||||
"details_show_addresses": "Mostrar direcciones",
|
||||
"details_title": "Billetera",
|
||||
|
@ -494,7 +498,9 @@
|
|||
"identity_pubkey": "Identidad Pubkey",
|
||||
"xpub_title": "XPUB de la billetera",
|
||||
"manage_wallets_search_placeholder": "Buscar billeteras, notas",
|
||||
"more_info": "Más información"
|
||||
"more_info": "Más información",
|
||||
"details_delete_wallet_error_message": "Hubo un problema al confirmar si esta billetera se eliminó de las notificaciones, lo que podría deberse a un problema de red o a una mala conexión. Si continúas, es posible que aún recibas notificaciones de transacciones relacionadas con esta billetera, incluso después de que se elimine.",
|
||||
"details_delete_anyway": "Borrar de todos modos"
|
||||
},
|
||||
"total_balance_view": {
|
||||
"display_in_bitcoin": "Mostrar en Bitcoin",
|
||||
|
@ -509,6 +515,10 @@
|
|||
"default_label": "Bóveda Multifirma",
|
||||
"multisig_vault_explain": "La mejor seguridad para grandes cantidades",
|
||||
"provide_signature": "Proporcionar firma",
|
||||
"provide_signature_details": "Usa tu dispositivo y billetera donde reside la llave para firmar esta transacción",
|
||||
"provide_signature_details_bluewallet": "En BlueWallet, ve al menú de la pantalla Enviar y selecciona",
|
||||
"provide_signature_next_steps": "Escanea o importa la transacción firmada",
|
||||
"provide_signature_next_steps_details": "Una vez que tu billetera haya firmado con éxito la transacción, escanea el código QR proporcionado o importa el archivo que lo acompaña, y luego revisa todos los detalles de la transacción antes de transmitirla.",
|
||||
"vault_key": "Clave de la Bóveda {number}",
|
||||
"required_keys_out_of_total": "Llaves requeridas del total",
|
||||
"fee": "Tarifa: {number}",
|
||||
|
@ -670,7 +680,7 @@
|
|||
"notification_tx_unconfirmed": "La transacción de notificación aún no está confirmada, espera",
|
||||
"failed_create_notif_tx": "No se pudo crear una transacción en cadena",
|
||||
"onchain_tx_needed": "Se necesita transacción en cadena",
|
||||
"notif_tx_sent" : "Transacción de notificación enviada. Espera a que se confirme",
|
||||
"notif_tx_sent": "Transacción de notificación enviada. Espera a que se confirme",
|
||||
"notif_tx": "Transacción de notificación",
|
||||
"not_found": "Código de pago no encontrado"
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
"storage_is_encrypted": "Sinu hoidla on krüpteeritud. Dekrüpteerimiseks on vajalik sisestada parool.",
|
||||
"yes": "Jah",
|
||||
"no": "Ei",
|
||||
"save": "Salvesta",
|
||||
"seed": "Seeme",
|
||||
"success": "Toiming õnnestus",
|
||||
"wallet_key": "Rahakoti võti"
|
||||
|
@ -41,7 +40,6 @@
|
|||
"create_to": "Sihtkoht"
|
||||
},
|
||||
"settings": {
|
||||
"electrum_clear_alert_cancel": "Katkesta",
|
||||
"save": "Salvesta"
|
||||
},
|
||||
"wallets": {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue