mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-13 19:16:52 +01:00
Compare commits
396 commits
Author | SHA1 | Date | |
---|---|---|---|
|
c7909049dc | ||
|
b5270d0a07 | ||
|
4f3b828990 | ||
|
26720e8284 | ||
|
a80bacc0f4 | ||
|
5f18540ca7 | ||
|
c14cb3508c | ||
|
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 | ||
|
e1202c6854 | ||
|
021a1fd352 | ||
|
8f26859f76 | ||
|
f6a6d7c41e |
197 changed files with 6240 additions and 4408 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,6 +22,7 @@ jobs:
|
|||
branch_name: ${{ steps.get_latest_commit_details.outputs.branch_name }}
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
MATCH_READONLY: "true"
|
||||
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
|
@ -29,6 +30,33 @@ jobs:
|
|||
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'
|
||||
run: |
|
||||
|
@ -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,38 +270,35 @@ 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
|
||||
|
||||
- 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({
|
||||
|
|
9
App.tsx
9
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>
|
||||
<LargeScreenProvider>
|
||||
<StorageProvider>
|
||||
<SettingsProvider>
|
||||
<MasterView />
|
||||
</SettingsProvider>
|
||||
</StorageProvider>
|
||||
</LargeScreenProvider>
|
||||
</SafeAreaProvider>
|
||||
</NavigationContainer>
|
||||
</LargeScreenProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
9
Gemfile
9
Gemfile
|
@ -3,9 +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)
|
||||
|
|
52
Gemfile.lock
52
Gemfile.lock
|
@ -24,17 +24,18 @@ GEM
|
|||
json (>= 1.5.1)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1042.0)
|
||||
aws-sdk-core (3.217.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.97.0)
|
||||
aws-sdk-kms (1.99.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.178.0)
|
||||
aws-sdk-s3 (1.182.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
|
@ -45,10 +46,10 @@ GEM
|
|||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.9)
|
||||
claide (1.1.0)
|
||||
cocoapods (1.15.2)
|
||||
cocoapods (1.14.3)
|
||||
addressable (~> 2.8)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.15.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)
|
||||
|
@ -63,7 +64,7 @@ GEM
|
|||
nap (~> 1.0)
|
||||
ruby-macho (>= 2.3.0, < 3.0)
|
||||
xcodeproj (>= 1.23.0, < 2.0)
|
||||
cocoapods-core (1.15.2)
|
||||
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.5)
|
||||
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)
|
||||
|
@ -172,6 +173,9 @@ GEM
|
|||
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)
|
||||
|
@ -179,6 +183,11 @@ GEM
|
|||
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)
|
||||
|
@ -219,24 +228,26 @@ GEM
|
|||
http-accept (1.7.0)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.9.1)
|
||||
json (2.10.1)
|
||||
jwt (2.10.1)
|
||||
base64
|
||||
logger (1.6.5)
|
||||
logger (1.6.6)
|
||||
mime-types (3.6.0)
|
||||
logger
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2025.0107)
|
||||
mime-types-data (3.2025.0220)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.4)
|
||||
molinillo (0.8.0)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
naturally (2.2.1)
|
||||
|
@ -245,8 +256,10 @@ GEM
|
|||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
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,7 +270,7 @@ GEM
|
|||
mime-types (>= 1.16, < 4.0)
|
||||
netrc (~> 0.8)
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.0)
|
||||
rexml (3.4.1)
|
||||
rouge (3.28.0)
|
||||
ruby-macho (2.5.1)
|
||||
ruby2_keywords (0.0.5)
|
||||
|
@ -299,16 +312,21 @@ GEM
|
|||
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)
|
||||
|
||||
|
|
|
@ -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.9"
|
||||
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"
|
|
@ -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
|
||||
|
|
|
@ -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()}")
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -138,6 +138,7 @@ async function _getRealm() {
|
|||
}
|
||||
|
||||
export const getPreferredServer = async (): Promise<ElectrumServerItem | undefined> => {
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
|
||||
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
|
||||
|
@ -155,14 +156,22 @@ export const getPreferredServer = async (): Promise<ElectrumServerItem | undefin
|
|||
tcp: tcpPort ? Number(tcpPort) : undefined,
|
||||
ssl: sslPort ? Number(sslPort) : undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in getPreferredServer:', error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const removePreferredServer = async () => {
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
console.log('Removing preferred server');
|
||||
await DefaultPreference.clear(ELECTRUM_HOST);
|
||||
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
|
||||
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
|
||||
} catch (error) {
|
||||
console.error('Error in removePreferredServer:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export async function isDisabled(): Promise<boolean> {
|
||||
|
@ -204,6 +213,7 @@ function getNextPeer() {
|
|||
}
|
||||
|
||||
async function getSavedPeer(): Promise<Peer | null> {
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
|
||||
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
|
||||
|
@ -224,6 +234,10 @@ async function getSavedPeer(): Promise<Peer | null> {
|
|||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error in getSavedPeer:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function connectMain(): Promise<void> {
|
||||
|
@ -239,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');
|
||||
|
@ -262,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
|
||||
|
@ -310,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 {
|
||||
|
@ -407,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',
|
||||
|
@ -418,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);
|
||||
}
|
||||
});
|
||||
|
@ -429,7 +436,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
|
|||
text: loc._.cancel,
|
||||
onPress: () => {
|
||||
connectionAttempt = 0;
|
||||
mainClient.close() && mainClient.close();
|
||||
mainClient?.close();
|
||||
mainClient = undefined;
|
||||
},
|
||||
style: 'cancel',
|
||||
},
|
||||
|
@ -474,6 +482,7 @@ async function getRandomDynamicPeer(): Promise<Peer> {
|
|||
}
|
||||
|
||||
export const getBalanceByAddress = async function (address: string): Promise<{ confirmed: number; unconfirmed: number }> {
|
||||
try {
|
||||
if (!mainClient) throw new Error('Electrum client is not connected');
|
||||
const script = bitcoin.address.toOutputScript(address);
|
||||
const hash = bitcoin.crypto.sha256(script);
|
||||
|
@ -481,6 +490,10 @@ export const getBalanceByAddress = async function (address: string): Promise<{ c
|
|||
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 () {
|
||||
|
@ -958,6 +971,7 @@ export async function multiGetTransactionByTxid<T extends boolean>(
|
|||
}
|
||||
|
||||
// saving cache:
|
||||
try {
|
||||
realm.write(() => {
|
||||
for (const txid of Object.keys(ret)) {
|
||||
const tx = ret[txid];
|
||||
|
@ -977,6 +991,9 @@ export async function multiGetTransactionByTxid<T extends boolean>(
|
|||
);
|
||||
}
|
||||
});
|
||||
} 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,12 +47,7 @@ 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}`;
|
||||
const filePath = `${RNFS.DownloadDirectoryPath}/${sanitizedFileName}`;
|
||||
try {
|
||||
await RNFS.writeFile(filePath, contents);
|
||||
if (showShareDialog) {
|
||||
|
@ -62,22 +56,12 @@ export const writeFileAndExport = async function (fileName: string, contents: st
|
|||
presentAlert({ message: loc.formatString(loc.send.file_saved_at_path, { filePath }) });
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
presentAlert({ message: e.message });
|
||||
}
|
||||
} 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 (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 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`, {
|
||||
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();
|
||||
} 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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) => {
|
||||
const onPressMenuItem = useCallback(
|
||||
(action: string) => {
|
||||
switch (action) {
|
||||
case CommonToolTipActions.ImportWallet.id:
|
||||
navigationRef.current?.navigate('AddWalletRoot', { screen: 'ImportWallet' });
|
||||
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}
|
||||
>
|
||||
{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 = () => {
|
||||
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,10 +98,16 @@ const CameraScreen: React.FC<CameraScreenProps> = ({
|
|||
|
||||
return (
|
||||
<View style={styles.screen}>
|
||||
{/* Render top buttons only if not desktop as they would not be relevant */}
|
||||
{!isDesktop && (
|
||||
<View style={styles.topButtons}>
|
||||
<TouchableOpacity style={styles.topButton} onPress={onSetTorch}>
|
||||
<Animated.View style={[styles.topButtonImg, uiRotationStyle]}>
|
||||
<Icon name={torchMode ? 'flashlight-on' : 'flashlight-off'} type="font-awesome-6" color="#ffffff" />
|
||||
<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}>
|
||||
|
@ -109,7 +118,7 @@ const CameraScreen: React.FC<CameraScreenProps> = ({
|
|||
style={[styles.topButton, styles.spacing, uiRotationStyle]}
|
||||
onPress={onImagePickerButtonPress}
|
||||
>
|
||||
<Animated.View style={[styles.topButtonImg, uiRotationStyle]}>
|
||||
<Animated.View style={styles.topButtonImg}>
|
||||
<Icon name="image" type="font-awesome" color="#ffffff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
|
@ -121,42 +130,72 @@ const CameraScreen: React.FC<CameraScreenProps> = ({
|
|||
style={[styles.topButton, styles.spacing, uiRotationStyle]}
|
||||
onPress={onFilePickerButtonPress}
|
||||
>
|
||||
<Animated.View style={[styles.topButtonImg, uiRotationStyle]}>
|
||||
<Animated.View style={styles.topButtonImg}>
|
||||
<Icon name="file-import" type="font-awesome-5" color="#ffffff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
)}
|
||||
<View style={styles.cameraContainer}>
|
||||
<Camera
|
||||
ref={cameraRef}
|
||||
style={styles.cameraPreview}
|
||||
cameraType={cameraType}
|
||||
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" />
|
||||
{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) {
|
||||
console.debug('[refreshAllWalletTransactions] Setting wallet transaction status to ALL');
|
||||
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');
|
||||
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,34 +76,26 @@ 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;
|
||||
};
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
ref={ref}
|
||||
onLayout={onLayout}
|
||||
style={[
|
||||
cStyles.root,
|
||||
props.inline ? cStyles.rootInline : cStyles.rootAbsolute,
|
||||
bottomInsets,
|
||||
newWidth ? cStyles.rootPost : cStyles.rootPre,
|
||||
{ transform: [{ translateY: slideAnimation }] },
|
||||
]}
|
||||
>
|
||||
{newWidth
|
||||
? React.Children.toArray(props.children)
|
||||
.filter(Boolean)
|
||||
.map((child, index, array) => {
|
||||
const renderChild = (child: ReactNode, index: number, array: ReactNode[]): ReactNode => {
|
||||
if (typeof child === 'string') {
|
||||
return (
|
||||
<View key={index} style={{ width: newWidth }}>
|
||||
|
@ -93,51 +110,45 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
|
|||
key: index,
|
||||
first: index === 0,
|
||||
last: index === array.length - 1,
|
||||
singleChild: array.length === 1,
|
||||
});
|
||||
})
|
||||
: props.children}
|
||||
};
|
||||
|
||||
const totalChildren = React.Children.toArray(props.children).filter(Boolean).length;
|
||||
return (
|
||||
<Animated.View
|
||||
ref={ref}
|
||||
onLayout={onLayout}
|
||||
style={[
|
||||
containerStyles.root,
|
||||
props.inline ? containerStyles.rootInline : containerStyles.rootAbsolute,
|
||||
bottomInsets,
|
||||
newWidth ? containerStyles.rootPost : containerStyles.rootPre,
|
||||
totalChildren === 1 ? containerStyles.rootRound : null,
|
||||
{ transform: [{ translateY: slideAnimation }] },
|
||||
]}
|
||||
>
|
||||
{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, Platform } 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 {
|
||||
|
@ -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 (
|
||||
<Animated.View style={animatedStyle}>
|
||||
<ListItem.Swipeable
|
||||
leftWidth={80}
|
||||
rightWidth={90}
|
||||
containerStyle={[{ backgroundColor: colors.background }, style]}
|
||||
leftContent={leftContent}
|
||||
rightContent={rightContent}
|
||||
Component={TouchableOpacityWrapper}
|
||||
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={isActive ? styles.activeItem : undefined}
|
||||
>
|
||||
<ListItem.Content
|
||||
style={{
|
||||
backgroundColor: colors.background,
|
||||
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>
|
||||
</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,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();
|
||||
success ? handleConfirmSuccess() : handleConfirmationFailure();
|
||||
} else {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
onConfirmationFailure();
|
||||
if (!isSuccess) {
|
||||
// Prevent shake animation if success is detected
|
||||
handleShakeAnimation();
|
||||
}
|
||||
}
|
||||
} 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,8 +269,12 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
/>
|
||||
</Animated.View>
|
||||
) : (
|
||||
<Animated.View style={[{ opacity: fadeOutAnimation, transform: [{ scale: scaleAnimation }] }, styles.feeModalFooter]}>
|
||||
{!isVisible && (
|
||||
<Animated.View
|
||||
style={[
|
||||
{ opacity: isVisible ? opacity : fadeOutAnimation, transform: [{ scale: scaleAnimation }] },
|
||||
styles.feeModalFooterSpacing,
|
||||
]}
|
||||
>
|
||||
<SecondButton
|
||||
title={isLoading ? '' : loc._.ok}
|
||||
onPress={handleSubmit}
|
||||
|
@ -291,7 +282,6 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
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>) => {
|
||||
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,19 +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, 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,
|
||||
|
@ -23,18 +19,6 @@ 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;
|
||||
|
@ -88,11 +72,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
|||
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 => {
|
||||
|
@ -119,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);
|
||||
|
@ -133,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
|
||||
|
@ -198,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();
|
||||
} else {
|
||||
if (item.ispaid) {
|
||||
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
|
||||
if (invoiceExpiration > now || item.ispaid) {
|
||||
return formattedAmount;
|
||||
} else {
|
||||
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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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,18 +272,21 @@ platform :ios do
|
|||
|
||||
desc "Synchronize certificates and provisioning profiles"
|
||||
lane :setup_provisioning_profiles do
|
||||
UI.message("Setting up provisioning profiles...")
|
||||
required_vars = ["GIT_ACCESS_TOKEN", "GIT_URL", "ITC_TEAM_ID", "ITC_TEAM_NAME", "KEYCHAIN_PASSWORD"]
|
||||
ensure_env_vars(required_vars)
|
||||
|
||||
platform = "ios"
|
||||
UI.message("Setting up provisioning profiles...")
|
||||
|
||||
# Iterate over app identifiers to fetch provisioning profiles
|
||||
app_identifiers.each do |app_identifier|
|
||||
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, # Skip if the branch already exists
|
||||
platform: platform,
|
||||
clone_branch_directly: true,
|
||||
platform: "ios",
|
||||
app_identifier: app_identifier,
|
||||
team_id: ENV["ITC_TEAM_ID"],
|
||||
team_name: ENV["ITC_TEAM_NAME"],
|
||||
|
@ -257,9 +294,13 @@ platform :ios do
|
|||
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"
|
||||
lane :fetch_dev_profiles_catalyst do
|
||||
match(
|
||||
|
@ -411,39 +452,105 @@ lane :build_app_lane do
|
|||
|
||||
clear_derived_data_lane
|
||||
|
||||
# Determine which iOS version to use
|
||||
ios_version = determine_ios_version
|
||||
|
||||
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",
|
||||
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",
|
||||
output_directory: ipa_directory,
|
||||
output_name: ipa_name,
|
||||
buildlog_path: File.join(project_root, "ios", "build_logs"),
|
||||
silent: false,
|
||||
clean: true
|
||||
)
|
||||
rescue => e
|
||||
UI.user_error!("build_ios_app failed: #{e.message}")
|
||||
end
|
||||
|
||||
# Use File.join to construct paths without extra slashes
|
||||
ipa_path = lane_context[SharedValues::IPA_OUTPUT_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}")
|
||||
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.")
|
||||
# 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
|
||||
# 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
|
||||
|
||||
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,18 @@ 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.navigate('ScanQRCode', {
|
||||
showFileImportButton: true,
|
||||
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,34 +183,48 @@ 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 (!decodedUrl) {
|
||||
throw new Error(loc.send.qr_error_no_qrcode);
|
||||
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 values = await RNQRGenerator.detect({
|
||||
uri: decodedUrl,
|
||||
});
|
||||
|
||||
if (values && values.values.length > 0) {
|
||||
const fileName = decodedUrl.split('/').pop()?.toLowerCase() || '';
|
||||
if (/\.(jpe?g|png)$/i.test(fileName)) {
|
||||
let qrResult;
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (qrResult?.values?.length) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
DeeplinkSchemaMatch.navigationRouteFor(
|
||||
{ url: values.values[0] },
|
||||
{ url: qrResult.values[0] },
|
||||
(value: [string, any]) => navigationRef.navigate(...value),
|
||||
{
|
||||
wallets,
|
||||
|
@ -205,11 +236,6 @@ const CompanionDelegates = () => {
|
|||
} 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 });
|
||||
}
|
||||
} else {
|
||||
DeeplinkSchemaMatch.navigationRouteFor(event, (value: [string, any]) => navigationRef.navigate(...value), {
|
||||
wallets,
|
||||
|
@ -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) {
|
||||
// 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();
|
||||
}
|
||||
|
|
|
@ -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?.()
|
||||
if (EventEmitter && EventEmitter.getMostRecentUserActivity) {
|
||||
EventEmitter.getMostRecentUserActivity()
|
||||
.then(handleUserActivity)
|
||||
.catch(() => console.debug('No userActivity object sent'));
|
||||
.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?.();
|
||||
},
|
||||
/**
|
||||
* 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,
|
||||
}),
|
||||
[dispatchNavigate],
|
||||
);
|
||||
}
|
||||
} 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(() => {
|
||||
|
|
|
@ -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 = (
|
||||
|
@ -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 = 1703137999;
|
||||
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.9;
|
||||
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 = 1703137999;
|
||||
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.9;
|
||||
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 = 1703137999;
|
||||
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.9;
|
||||
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 = 1703137999;
|
||||
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.9;
|
||||
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 = 1703137999;
|
||||
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.9;
|
||||
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 = 1703137999;
|
||||
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.9;
|
||||
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 = 1703137999;
|
||||
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.9;
|
||||
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 = 1703137999;
|
||||
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.9;
|
||||
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 = 1703137999;
|
||||
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.9;
|
||||
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 = 1703137999;
|
||||
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.9;
|
||||
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()
|
||||
}
|
||||
}
|
605
ios/Podfile.lock
605
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",
|
||||
|
|
|
@ -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": "Πορτοφόλι",
|
||||
|
|
17
loc/en.json
17
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",
|
||||
|
@ -285,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.",
|
||||
|
@ -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",
|
||||
|
|
|
@ -136,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",
|
||||
|
@ -182,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",
|
||||
|
@ -239,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",
|
||||
|
@ -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.",
|
||||
|
@ -422,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",
|
||||
|
@ -493,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",
|
||||
|
@ -508,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}",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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_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_decrypt_q": "آیا از رمزگشایی فضای ذخیرهسازی خود اطمینان دارید؟ این کار اجازه میدهد تا کیف پولهای شما بدون گذرواژه قابلدسترسی باشند.",
|
||||
"encrypt_enc_and_pass": "رمزگذاری و محافظت با گذرواژه",
|
||||
|
@ -338,7 +332,6 @@
|
|||
"details_export_history": "گرفتن خروجی تاریخچه به فرمت CSV",
|
||||
"details_master_fingerprint": "اثر انگشت اصلی",
|
||||
"details_multisig_type": "چندامضایی",
|
||||
"details_no_cancel": "خیر، لغو کن",
|
||||
"details_show_xpub": "نمایش XPUB کیف پول",
|
||||
"details_show_addresses": "نمایش آدرسها",
|
||||
"details_title": "کیف پول",
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
"storage_is_encrypted": "Tallennustilasi on salattu. Sen purkamiseksi vaaditaan salasana",
|
||||
"yes": "Kyllä",
|
||||
"no": "Ei",
|
||||
"save": "Tallenna",
|
||||
"seed": "Siemen",
|
||||
"success": "Onnistui",
|
||||
"wallet_key": "Lompakkoavain",
|
||||
|
@ -177,7 +176,6 @@
|
|||
"outdated_rate": "Vaihtokurssi päivitettiin viimeksi: {date}",
|
||||
"psbt_tx_open": "Avaa allekirjoitettu siirtotapahtuma",
|
||||
"psbt_tx_scan": "Skannaa allekirjoitettu siirtotapahtuma",
|
||||
"qr_error_no_qrcode": "Kuvasta ei löytynyt QR-koodia. Varmista että kuva sisältää ainoastaan QR-koodin eikä muita tietoja kuten tekstia tai nappeja.",
|
||||
"reset_amount": "Nollaa määrä",
|
||||
"reset_amount_confirm": "Haluaisitko nollata määrän?",
|
||||
"success_done": "Valmis",
|
||||
|
@ -223,13 +221,8 @@
|
|||
"set_electrum_server_as_default": "Asetetaanko {server} oletus Electrum-palvelimeksi?",
|
||||
"electrum_settings_server": "Electrum-palvelin",
|
||||
"electrum_status": "Tila",
|
||||
"electrum_clear_alert_title": "Tyhjennä historia?",
|
||||
"electrum_clear_alert_message": "Haluatko tyhjentää Electrum-palvelinten historian?",
|
||||
"electrum_clear_alert_cancel": "Peruuta",
|
||||
"electrum_clear_alert_ok": "Ok",
|
||||
"electrum_reset": "Palauta oletusasetuksiin",
|
||||
"electrum_unable_to_connect": " Ei saada yhteyttä {server}. ",
|
||||
"electrum_reset_to_default": "Haluatko varmasti palauttaa Electrumin asetukset oletusarvoihin? ",
|
||||
"electrum_reset": "Palauta oletusasetuksiin",
|
||||
"encrypt_decrypt": "Pura tallennustilan salaus",
|
||||
"encrypt_decrypt_q": "Haluatko varmasti purkaa tallennustilan salauksen? Tämä mahdollistaa lompakkoihisi pääsyn ilman salasanaa.",
|
||||
"encrypt_enc_and_pass": "Salattu ja Salasanalla suojattu",
|
||||
|
@ -357,7 +350,6 @@
|
|||
"details_export_history": "Vie historia CSV:ksi",
|
||||
"details_master_fingerprint": "Pää sormenjälki",
|
||||
"details_multisig_type": "multisig",
|
||||
"details_no_cancel": "Ei, peruuta",
|
||||
"details_show_xpub": "Näytä lompakon XPUB",
|
||||
"details_show_addresses": "Näytä osoitteet",
|
||||
"details_title": "Lompakko",
|
||||
|
|
|
@ -10,12 +10,10 @@
|
|||
"never": "Jamais",
|
||||
"of": "{number} sur {total}",
|
||||
"ok": "OK",
|
||||
"customize": "Personnaliser",
|
||||
"enter_url": "Entrer une URL",
|
||||
"storage_is_encrypted": "L'espace de stockage est chiffré. le mot de passe est requis pour le déchiffrer.",
|
||||
"yes": "Oui",
|
||||
"no": "Non",
|
||||
"save": "Enregistrer",
|
||||
"seed": "Graine",
|
||||
"success": "Succès",
|
||||
"wallet_key": "Clé du portefeuille",
|
||||
|
@ -255,8 +253,9 @@
|
|||
"electrum_preferred_server_description": "Saisissez le serveur que vous souhaitez que votre portefeuille utilise pour toutes les activités Bitcoin. Une fois défini, votre portefeuille utilisera exclusivement ce serveur pour vérifier les soldes, envoyer des transactions et récupérer les données du réseau. Assurez-vous de faire confiance à ce serveur avant de le configurer. ",
|
||||
"electrum_unable_to_connect": "Impossible de se connecter à {server}.",
|
||||
"electrum_history": "Historique",
|
||||
"electrum_reset_to_default": "Cela permettra à BlueWallet de choisir au hasard un serveur dans la liste et l'historique suggérés. L'historique de votre serveur restera inchangé. ",
|
||||
"electrum_reset_to_default": "Cela permettra à BlueWallet de choisir au hasard un serveur dans la liste des serveurs.",
|
||||
"electrum_reset": "Réinitialiser au valeurs par défaut",
|
||||
"electrum_reset_to_default_and_clear_history": "Réinitialiser les paramètres par défaut et effacer l'historique",
|
||||
"encrypt_decrypt": "Déchiffrer le stockage",
|
||||
"encrypt_decrypt_q": "Etes-vous sûr de vouloir déchiffrer le stockage ? L'accès à vos portefeuilles pourra alors se faire sans mot de passe.",
|
||||
"encrypt_enc_and_pass": "Chiffré et protégé par un mot de passe",
|
||||
|
@ -403,7 +402,6 @@
|
|||
"add_wallet_name": "nom",
|
||||
"add_wallet_type": "type",
|
||||
"add_wallet_seed_length": "Longueur de graîne",
|
||||
"add_wallet_seed_length_message": "Choisissez la longueur de la phrase de départ que vous souhaitez utiliser pour ce portefeuille.",
|
||||
"add_wallet_seed_length_12": "12 mots",
|
||||
"add_wallet_seed_length_24": "24 mots",
|
||||
"clipboard_bitcoin": "Vous avez une adresse bitcoin dans votre presse-papier. Voulez vous l'utiliser pour une transaction ?",
|
||||
|
@ -423,7 +421,6 @@
|
|||
"details_export_history": "Exporter l'historique au format CSV",
|
||||
"details_master_fingerprint": "Empreinte maitresse",
|
||||
"details_multisig_type": "multisig",
|
||||
"details_no_cancel": "Non, annuler",
|
||||
"details_show_xpub": "Afficher XPUB du portefeuille",
|
||||
"details_show_addresses": "Montrer les adresses",
|
||||
"details_title": "Portefeuille",
|
||||
|
|
25
loc/he.json
25
loc/he.json
|
@ -10,12 +10,10 @@
|
|||
"never": "אף פעם",
|
||||
"of": "{number} מתוך {total}",
|
||||
"ok": "אישור",
|
||||
"customize": "התאמה אישית",
|
||||
"enter_url": "הכנסת URL",
|
||||
"storage_is_encrypted": "האחסון שלך מוצפן. נדרשת סיסמה לפענוח שלו.",
|
||||
"yes": "כן",
|
||||
"no": "לא",
|
||||
"save": "שמירה",
|
||||
"seed": "גרעין",
|
||||
"success": "הצלחה",
|
||||
"wallet_key": "מפתח ארנק",
|
||||
|
@ -28,6 +26,8 @@
|
|||
"enter_amount": "הכנסת סכום",
|
||||
"qr_custom_input_button": "הקישו 10 פעמים כדי להכניס קלט מותאם",
|
||||
"unlock": "פתיחה",
|
||||
"port": "פתחה",
|
||||
"ssl_port": "פתחת SSL",
|
||||
"suggested": "מוצע"
|
||||
},
|
||||
"azteco": {
|
||||
|
@ -72,6 +72,7 @@
|
|||
"open_direct_channel": "פתח ערוץ ישיר עם צומת זה:",
|
||||
"please_pay_between_and": "אנא שלמו בין {min} לבין {max}",
|
||||
"please_pay": "אנא שלמו",
|
||||
"date_time": "תאריך ושעה",
|
||||
"wasnt_paid_and_expired": "חשבונית זו לא שולמה ופגה"
|
||||
},
|
||||
"plausibledeniability": {
|
||||
|
@ -103,8 +104,7 @@
|
|||
"maxSatsFull": "סכום מקסימלי הינו {max} sats או {currency}",
|
||||
"minSats": "סכום מינימלי הינו {min} sats",
|
||||
"minSatsFull": "סכום מינימלי הינו {min} sats או {currency}",
|
||||
"qrcode_for_the_address": "קוד QR לכתובת",
|
||||
"bip47_explanation": "קודי תשלום הם כתובת אוניברסלית שמונעת חשיפה של כתובות הארנק שלך. לא כל השירותים יתמכו בהם."
|
||||
"qrcode_for_the_address": "קוד QR לכתובת"
|
||||
},
|
||||
"send": {
|
||||
"provided_address_is_invoice": "נראה שכתובת זו היא חשבונית ברק. אנא עברו לארנק הברק שלכם כדי לבצע תשלום עבור חשבונית זו.",
|
||||
|
@ -188,7 +188,6 @@
|
|||
"outdated_rate": "תעריף עודכן לאחרונה: {date}",
|
||||
"psbt_tx_open": "פתחו פעולה חתומה",
|
||||
"psbt_tx_scan": "סרקו פעולה חתומה",
|
||||
"qr_error_no_qrcode": "לא הצלחנו למצוא קוד QR בתמונה הנבחרת. אנא ודאו כי התמונה מכילה רק קוד QR, ולא תוכן נוסף כמו טקסט, או כפתורים.",
|
||||
"reset_amount": "איפוס סכום",
|
||||
"reset_amount_confirm": "האם ברצונך לאפס את הסכום?",
|
||||
"success_done": "בוצע",
|
||||
|
@ -246,15 +245,9 @@
|
|||
"electrum_settings_server": "שרת אלקטרום",
|
||||
"electrum_status": "מצב",
|
||||
"electrum_preferred_server": "שרת מועדף",
|
||||
"electrum_clear_alert_title": "ניקוי היסטוריה?",
|
||||
"electrum_clear_alert_message": "האם ברצונך לנקות היסטורית שרתי אלקטרום?",
|
||||
"electrum_clear_alert_cancel": "ביטול",
|
||||
"electrum_clear_alert_ok": "אישור",
|
||||
"electrum_reset": "איפוס ברירת מחדל",
|
||||
"electrum_unable_to_connect": "לא מסוגל להתחבר לשרת {server}.",
|
||||
"electrum_history": "היסטוריה",
|
||||
"electrum_reset_to_default": "האם אתם בטוחים שברצונכם לשחזר את הגדרות האקלטרום שלכם לברירת מחדל?",
|
||||
"electrum_clear": "ניקוי היסטוריה",
|
||||
"electrum_reset": "איפוס ברירת מחדל",
|
||||
"encrypt_decrypt": "פתיחת אחסון מוצפן",
|
||||
"encrypt_decrypt_q": "האם לפענח אחסון מוצפן? זה יאפשר לגשת לארנקים שלך ללא סיסמה.",
|
||||
"encrypt_enc_and_pass": "מוצפן ומוגן על ידי סיסמה",
|
||||
|
@ -266,6 +259,7 @@
|
|||
"encrypt_title": "אבטחה",
|
||||
"encrypt_tstorage": "אחסון",
|
||||
"encrypt_use": "השתמש {type}",
|
||||
"set_as_preferred": "הגדרה כמועדף",
|
||||
"encrypted_feature_disabled": "לא ניתן להשתמש בתכונה זאת עם הצפנת אחסון מופעלת.",
|
||||
"encrypt_use_expl": "{type} ישמש לאמת את זהותך לפני ביצוע פעולה, פתיחה, יצוא או מחיקה של ארנק. {type} אינו ישמש לפתיחת אחסון מוצפן.",
|
||||
"biometrics_fail": "אם {type} לא מאופשר, או נכשל בפתיחה, תוכלו להשתמש בסיסמת המכשיר שלכם בתור חלופה.",
|
||||
|
@ -391,7 +385,6 @@
|
|||
"add_wallet_name": "שם",
|
||||
"add_wallet_type": "סוג",
|
||||
"add_wallet_seed_length": "אורך גרעין",
|
||||
"add_wallet_seed_length_message": "בחרו את אורך צירוף הגרעין שברצונכם להשתמש בשביל ארנק זה.",
|
||||
"add_wallet_seed_length_12": "12 מילים",
|
||||
"add_wallet_seed_length_24": "24 מילים",
|
||||
"clipboard_bitcoin": "ישנה כתובת ביטקוין בלוח. האם תרצו להשתמש בה בשביל העברה?",
|
||||
|
@ -411,7 +404,6 @@
|
|||
"details_export_history": "יצוא היסטוריה ל- CSV",
|
||||
"details_master_fingerprint": "טביעת אצבע ראשית",
|
||||
"details_multisig_type": "רב-חתימות",
|
||||
"details_no_cancel": "לא, בטל",
|
||||
"details_show_xpub": "הצגת מפתח צפייה של הארנק",
|
||||
"details_show_addresses": "הצגת כתובות",
|
||||
"details_title": "ארנק",
|
||||
|
@ -469,6 +461,10 @@
|
|||
"select_wallet": "בחירת ארנק",
|
||||
"xpub_copiedToClipboard": "הועתק ללוח.",
|
||||
"pull_to_refresh": "משכו כדי לרענן",
|
||||
"warning_do_not_disclose": "לעולם אל תשתפו את המידע למטה",
|
||||
"write_down_header": "יצירת גיבוי ידני",
|
||||
"wallet_type_this": "סוג ארנק זה הוא {type}.",
|
||||
"share_number": "שיתוף {number}",
|
||||
"add_ln_wallet_first": "עלייך להוסיף ארנק ברק קודם.",
|
||||
"identity_pubkey": "מפתח זהות ציבורי",
|
||||
"xpub_title": "מפתח צפייה של הארנק",
|
||||
|
@ -629,6 +625,7 @@
|
|||
"bip47": {
|
||||
"payment_code": "קוד תשלום",
|
||||
"contacts": "אנשי קשר",
|
||||
"bip47_explain": "קוד רב פעמי ובר שיתוף",
|
||||
"purpose": "קוד רב-פעמי ובר-שיתוף (BIP47)",
|
||||
"pay_this_contact": "תשלום לאיש קשר זה",
|
||||
"rename_contact": "שינוי שם איש קשר",
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
"storage_is_encrypted": "Vaš spremnik je kriptiran. Za dekripcoju je potrebna lozinka.",
|
||||
"yes": "Da",
|
||||
"no": "Ne",
|
||||
"save": "Spremi",
|
||||
"seed": "Izvor",
|
||||
"success": "Uspjeh"
|
||||
},
|
||||
|
@ -68,7 +67,6 @@
|
|||
"settings": {
|
||||
"about": "Informacije",
|
||||
"currency": "Valuta",
|
||||
"electrum_clear_alert_cancel": "Otkaži",
|
||||
"header": "Postavke",
|
||||
"language": "Jezik",
|
||||
"lightning_settings": "Lightning postavke",
|
||||
|
@ -95,7 +93,6 @@
|
|||
"details_are_you_sure": "Jesi li ziher?",
|
||||
"details_delete": "Obriši",
|
||||
"details_export_backup": "Izvoz / bekap",
|
||||
"details_no_cancel": "Ne, otiaži",
|
||||
"details_show_xpub": "Prikaži voletov XPUB",
|
||||
"details_title": "Volet",
|
||||
"wallets": "Voleti",
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"storage_is_encrypted": "A Tárhely titkosítva. Jelszó szükséges a dekódoláshoz",
|
||||
"yes": "Igen",
|
||||
"no": "Nem",
|
||||
"save": "Mentés",
|
||||
"seed": "jelszó sorozat",
|
||||
"success": "Sikeres",
|
||||
"wallet_key": "Tárca kulcs",
|
||||
|
@ -162,7 +161,6 @@
|
|||
"outdated_rate": "A ráta utoljára frissítve: {date}",
|
||||
"psbt_tx_open": "Aláírt tranzakció megnyitása",
|
||||
"psbt_tx_scan": "Aláírt tranzakció szkennelése",
|
||||
"qr_error_no_qrcode": "Nem találtunk QR kódot a kiválasztott képen. Győződjön meg arról, hogy a kép csak QR kódot tartalmaz, és nem tartalmaz további tartalmat, például szöveget vagy gombokat.",
|
||||
"reset_amount": "Összeg Visszaállítása",
|
||||
"reset_amount_confirm": "Valóban visszaállítja az összeget?",
|
||||
"success_done": "Kész!",
|
||||
|
@ -206,13 +204,8 @@
|
|||
"set_electrum_server_as_default": "{server} bállítása alapértelmezett Electrum szerverként?",
|
||||
"electrum_settings_server": "Electrum Szerver",
|
||||
"electrum_status": "Állapot",
|
||||
"electrum_clear_alert_title": "Előzmények törlése?",
|
||||
"electrum_clear_alert_message": "Kiszeretné törölni az Electrum Szerver előzményeket?",
|
||||
"electrum_clear_alert_cancel": "Mégse",
|
||||
"electrum_clear_alert_ok": "OK",
|
||||
"electrum_reset": "alapértelmezett beállítások visszaállítása",
|
||||
"electrum_unable_to_connect": "Nincs kapcsolat {server} -hez.",
|
||||
"electrum_reset_to_default": "Biztosan vissza akarja alapértelmezettre állitani az Electrum szerver beállításait?",
|
||||
"electrum_reset": "alapértelmezett beállítások visszaállítása",
|
||||
"encrypt_decrypt": "Háttértár titkosításának feloldása",
|
||||
"encrypt_decrypt_q": "Biztosan feloldod a háttértár titkosítását? Ez lehetővé teszi a tárca jelszó nélküli használatát. ",
|
||||
"encrypt_enc_and_pass": "Titkosítva és jelszóval védve",
|
||||
|
@ -333,7 +326,6 @@
|
|||
"details_export_backup": "Exportálás / Biztonsági mentés",
|
||||
"details_master_fingerprint": "Mester ujjlenyomat ",
|
||||
"details_multisig_type": "multisig",
|
||||
"details_no_cancel": "Nem, megszakít",
|
||||
"details_show_xpub": "Mutasd a tárca XPUB kulcsát",
|
||||
"details_show_addresses": "Cím mutatása",
|
||||
"details_title": "Tárca",
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"storage_is_encrypted": "Ruang penyimpanan terenkripsi. Masukkan kata sandi untuk decript:",
|
||||
"yes": "Ya",
|
||||
"no": "Tidak",
|
||||
"save": "Simpan",
|
||||
"seed": "Benih",
|
||||
"success": "Sukses",
|
||||
"wallet_key": "Kunci Dompet",
|
||||
|
@ -200,13 +199,8 @@
|
|||
"use_ssl": "Gunakan SSL",
|
||||
"electrum_settings_server": "Server Electrum",
|
||||
"electrum_status": "Status",
|
||||
"electrum_clear_alert_title": "Hapus riwayat?",
|
||||
"electrum_clear_alert_message": "Apakah Anda ingin menghapus riwayat server Electrum?",
|
||||
"electrum_clear_alert_cancel": "Batalkan",
|
||||
"electrum_clear_alert_ok": "OK",
|
||||
"electrum_reset": "Atur ulang ke bawaan",
|
||||
"electrum_unable_to_connect": "Tidak bisa mengubungkan ke {server}.",
|
||||
"electrum_reset_to_default": "Apakah Anda yakin ingin mengatur ulang Electrum Anda ke pengaturan bawaan?",
|
||||
"electrum_reset": "Atur ulang ke bawaan",
|
||||
"encrypt_enc_and_pass": "Dienkripsi dan kata sandi dilindungi",
|
||||
"encrypt_title": "Keamanan",
|
||||
"encrypt_tstorage": "Penyimpanan",
|
||||
|
@ -284,7 +278,6 @@
|
|||
"details_export_backup": "Ekspor / backup",
|
||||
"details_export_history": "Ekspor riwayat ke CSV",
|
||||
"details_master_fingerprint": "Sidik Jari Utama",
|
||||
"details_no_cancel": "Tidak, batalkan",
|
||||
"details_show_xpub": "Tampilkan XPUB dompet",
|
||||
"details_show_addresses": "Tunjukkan alamat",
|
||||
"details_title": "Dompet",
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"storage_is_encrypted": "Il tuo archivio è criptato. È necessaria una password per decriptarlo.",
|
||||
"yes": "Sì",
|
||||
"no": "No",
|
||||
"save": "Salva",
|
||||
"seed": "Seed",
|
||||
"success": "Operazione avvenuta con successo",
|
||||
"wallet_key": "Chiave del portafoglio",
|
||||
|
@ -207,13 +206,8 @@
|
|||
"set_electrum_server_as_default": "Impostare {server} come server Electrum predefinito?",
|
||||
"electrum_settings_server": "Server Electrum",
|
||||
"electrum_status": "Stato",
|
||||
"electrum_clear_alert_title": "Cancella la cronologia?",
|
||||
"electrum_clear_alert_message": "Desideri eliminare la cronologia dei server Electrum?",
|
||||
"electrum_clear_alert_cancel": "Annulla",
|
||||
"electrum_clear_alert_ok": "Ok",
|
||||
"electrum_reset": "Ripristino delle impostazioni predefinite",
|
||||
"electrum_unable_to_connect": "Impossibile collegarsi a {server}.",
|
||||
"electrum_reset_to_default": "Desideri veramente ripristinare le impostazioni Electrum predefinite?",
|
||||
"electrum_reset": "Ripristino delle impostazioni predefinite",
|
||||
"encrypt_decrypt": "Decripta l'archivio",
|
||||
"encrypt_decrypt_q": "Desideri veramente decriptare il tuo archivio? Quest'azione avrà come conseguenza di permettere l'accesso ai portafogli senza una password.",
|
||||
"encrypt_enc_and_pass": "Criptato e protetto da password",
|
||||
|
@ -333,7 +327,6 @@
|
|||
"details_export_history": "Esporta la cronologia in un file CSV",
|
||||
"details_master_fingerprint": "Master Fingerprint",
|
||||
"details_multisig_type": "multisig",
|
||||
"details_no_cancel": "No, annulla",
|
||||
"details_show_xpub": "Mostra XPUB del portafoglio",
|
||||
"details_show_addresses": "Mostra indirizzi",
|
||||
"details_title": "Portafoglio",
|
||||
|
|
|
@ -10,12 +10,11 @@
|
|||
"never": "データなし",
|
||||
"of": "{number} / {total}",
|
||||
"ok": "OK",
|
||||
"customize": "カスタマイズ",
|
||||
"enter_url": "URLを入力",
|
||||
"storage_is_encrypted": "ウォレットは暗号化されています。復号にはパスワードが必要です。",
|
||||
"yes": "はい",
|
||||
"no": "いいえ",
|
||||
"save": "保存",
|
||||
"save": "保存...",
|
||||
"seed": "シード",
|
||||
"success": "成功",
|
||||
"wallet_key": "ウォレットキー",
|
||||
|
@ -28,6 +27,8 @@
|
|||
"enter_amount": "額を入力",
|
||||
"qr_custom_input_button": "10回タップしてカスタム入力",
|
||||
"unlock": "ロック解除",
|
||||
"port": "ポート",
|
||||
"ssl_port": "SSLポート",
|
||||
"suggested": "サジェスト"
|
||||
},
|
||||
"azteco": {
|
||||
|
@ -74,6 +75,7 @@
|
|||
"please_pay": "取引額",
|
||||
"preimage": "プリイメージ",
|
||||
"sats": "sats",
|
||||
"date_time": "日時",
|
||||
"wasnt_paid_and_expired": "この請求書は支払いが行われなかったため無効になりました"
|
||||
},
|
||||
"plausibledeniability": {
|
||||
|
@ -134,6 +136,9 @@
|
|||
"details_add_recc_rem_all_alert_description": "本当に宛先をすべて削除しますか?",
|
||||
"details_add_rec_rem_all": "宛先をすべて削除",
|
||||
"details_recipients_title": "宛先",
|
||||
"details_recipient_title": "宛先#{number}/#{total}",
|
||||
"please_complete_recipient_title": "宛先が不完全",
|
||||
"please_complete_recipient_details": "新しい宛先を追加する前に、宛先#{number}の詳細を入力してください。",
|
||||
"details_address": "アドレス",
|
||||
"details_address_field_is_not_valid": "アドレス欄が正しくありません",
|
||||
"details_adv_fee_bump": "費用のバンプ(増加)を許可",
|
||||
|
@ -180,6 +185,7 @@
|
|||
"input_total": "合計:",
|
||||
"permission_camera_message": "カメラを使用するのに許可が必要です",
|
||||
"psbt_sign": "トランザクションに署名する",
|
||||
"invalid_psbt": "無効なPSBTが入力されました。",
|
||||
"open_settings": "設定を開く",
|
||||
"permission_storage_denied_message": "BlueWalletはファイルを保存できませんでした。デバイスの設定を開いてストレージのパーミッションを有効にしてください。",
|
||||
"permission_storage_title": "ストレージアクセス許可",
|
||||
|
@ -190,7 +196,7 @@
|
|||
"outdated_rate": "レートの最終更新:{date}",
|
||||
"psbt_tx_open": "署名トランザクションを開く",
|
||||
"psbt_tx_scan": "署名トランザクションをスキャン",
|
||||
"qr_error_no_qrcode": "選択した画像からQRコードが見つかりませんでした。画像がQRコードのみを含み、テキストやボタンなど別の内容を含まないようにしてください。",
|
||||
"qr_error_no_qrcode": "選択した画像から有効なQRコードが見つかりませんでした。画像がQRコードのみを含み、テキストやボタンなど別の内容を含まないようにしてください。",
|
||||
"reset_amount": "額をリセット",
|
||||
"reset_amount_confirm": "額をリセットしますか?",
|
||||
"success_done": "完了",
|
||||
|
@ -237,6 +243,7 @@
|
|||
"electrum_connected": "接続済",
|
||||
"electrum_connected_not": "未接続",
|
||||
"electrum_error_connect": "指定されたElectrumサーバーに接続できません",
|
||||
"electrum_error_connect_tor": "入力されたElectrumサーバーに接続できません。Orbitアプリが接続されていることを確認して再度お試しください。",
|
||||
"lndhub_uri": "例:{example}",
|
||||
"electrum_host": "例:{example}",
|
||||
"electrum_offline_mode": "オフラインモード",
|
||||
|
@ -250,15 +257,11 @@
|
|||
"electrum_status": "ステータス",
|
||||
"electrum_preferred_server": "優先サーバー",
|
||||
"electrum_preferred_server_description": "あなたのウォレットのすべてのビットコイン取引に使うサーバーを入力します。設定すると、あなたのウォレットは残高の確認、トランザクションの送信、ネットワークデータの取得にこのサーバーだけを使います。信用するサーバーを設定するようにしてください。",
|
||||
"electrum_clear_alert_title": "履歴を削除しますか?",
|
||||
"electrum_clear_alert_message": "Electrumサーバーヒストリーをクリアしますか?",
|
||||
"electrum_clear_alert_cancel": "キャンセル",
|
||||
"electrum_clear_alert_ok": "Ok",
|
||||
"electrum_reset": "デフォルトの設定に戻す",
|
||||
"electrum_unable_to_connect": "{server}に接続できません。",
|
||||
"electrum_history": "履歴",
|
||||
"electrum_reset_to_default": "Electrumの設定をデフォルトに戻してよろしいですか?",
|
||||
"electrum_clear": "履歴を削除",
|
||||
"electrum_reset_to_default": "サーバーリストからBlueWalletがランダムにサーバーを選択するようになります。",
|
||||
"electrum_reset": "デフォルトの設定に戻す",
|
||||
"electrum_reset_to_default_and_clear_history": "デフォルトにリセットして履歴を削除",
|
||||
"encrypt_decrypt": "ストレージ復号化",
|
||||
"encrypt_decrypt_q": "本当にストレージを復号化しますか?これによりウォレットがパスワードなしでアクセス可能になります。",
|
||||
"encrypt_enc_and_pass": "暗号化しパスワードで保護",
|
||||
|
@ -272,6 +275,8 @@
|
|||
"encrypt_title": "セキュリティ",
|
||||
"encrypt_tstorage": "ストレージ",
|
||||
"encrypt_use": "{type} を使う",
|
||||
"set_as_preferred": "優先に設定",
|
||||
"set_as_preferred_electrum": "優先サーバーとして {host}:{port} を設定することで、おすすめサーバーへのランダムな接続が無効になります。",
|
||||
"encrypted_feature_disabled": "ストレージの暗号化が有効の場合この機能は使えません。",
|
||||
"encrypt_use_expl": "{type} は、トランザクションの実行、ウォレットのロック解除、エクスポート、または削除を行う前の本人確認に使用されます。{type} は暗号化されたストレージのロック解除には使用されません。",
|
||||
"biometrics_fail": "もし {type} が有効でない、または解除に失敗した場合、デバイスのパスコードを代わりに使うことができます。",
|
||||
|
@ -284,13 +289,15 @@
|
|||
"last_updated": "最終更新",
|
||||
"language_isRTL": "言語の向きを適用するにはBlueWalletの再起動が必要です。",
|
||||
"license": "ライセンス",
|
||||
"lightning_error_lndhub_uri": "無効なLndHub URIです",
|
||||
"lightning_error_lndhub_uri": "無効なLNDhub URIです",
|
||||
"lightning_error_lndhub_uri_tor": "無効なLNDhub URIです。Orbotアプリが接続されていることを確認して再度お試しください。",
|
||||
"lightning_saved": "変更は正常に保存されました",
|
||||
"lightning_settings": "Lightning 設定",
|
||||
"lightning_settings_explain": "他の LND ノードへ接続するには LNDHub をインストール後、URL を入力してください。指定した LNDHub へ接続するのは変更保存後に作成されたウォレットのみですので注意してください。",
|
||||
"network": "ネットワーク",
|
||||
"network_broadcast": "ブロードキャストトランザクション",
|
||||
"network_electrum": "Electrum サーバー",
|
||||
"electrum_suggested_description": "優先サーバーが設定されていない場合、ランダムに選ばれたおすすめのサーバーが使われます。",
|
||||
"not_a_valid_uri": "無効なURI",
|
||||
"notifications": "通知",
|
||||
"open_link_in_explorer": "エクスプローラで開く",
|
||||
|
@ -402,7 +409,6 @@
|
|||
"add_wallet_name": "ウォレット名",
|
||||
"add_wallet_type": "タイプ",
|
||||
"add_wallet_seed_length": "シード長",
|
||||
"add_wallet_seed_length_message": "このウォレットで使いたいシードフレーズの長さを選択してください。",
|
||||
"add_wallet_seed_length_12": "12語",
|
||||
"add_wallet_seed_length_24": "24語",
|
||||
"clipboard_bitcoin": "クリップボードにビットコインのアドレスがあります。このアドレスを使って取引をしますか?",
|
||||
|
@ -422,7 +428,6 @@
|
|||
"details_export_history": "履歴をCSVにエクスポート",
|
||||
"details_master_fingerprint": "マスタフィンガープリント",
|
||||
"details_multisig_type": "マルチシグ",
|
||||
"details_no_cancel": "いいえ、中止します",
|
||||
"details_show_xpub": "ウォレット XPUB の表示",
|
||||
"details_show_addresses": "アドレスを表示",
|
||||
"details_title": "ウォレット",
|
||||
|
@ -493,7 +498,9 @@
|
|||
"identity_pubkey": "識別用公開鍵",
|
||||
"xpub_title": "ウォレット XPUB",
|
||||
"manage_wallets_search_placeholder": "ウォレット・メモを検索",
|
||||
"more_info": "詳細情報"
|
||||
"more_info": "詳細情報",
|
||||
"details_delete_wallet_error_message": "ウォレットが通知から削除されたかの確認に問題が生じました—ネットワークの問題か、接続が弱いためかもしれません。続行すると、ウォレットを削除した後でも、関連するトランザクションの通知を受け取る可能性があります。",
|
||||
"details_delete_anyway": "とにかく削除"
|
||||
},
|
||||
"total_balance_view": {
|
||||
"display_in_bitcoin": "ビットコインで表示",
|
||||
|
@ -508,6 +515,10 @@
|
|||
"default_label": "マルチシグ金庫",
|
||||
"multisig_vault_explain": "大きな資産にベストなセキュリティ",
|
||||
"provide_signature": "署名を提供",
|
||||
"provide_signature_details": "このトランザクションに署名するためのキーを持っているデバイスとウォレットを使ってください",
|
||||
"provide_signature_details_bluewallet": "BlueWalletで、送金スクリーンメニューに移動し",
|
||||
"provide_signature_next_steps": "署名済みトランザクションをスキャンまたはインポート",
|
||||
"provide_signature_next_steps_details": "ウォレットを使ったトランザクションの署名が終わったら、QRコードをスキャンまたはファイルをインポートして、トランザクションの詳細をすべて確認してから配信してください。",
|
||||
"vault_key": "金庫キー {number}",
|
||||
"required_keys_out_of_total": "全体のうち必要なキー",
|
||||
"fee": "費用: {number}",
|
||||
|
@ -654,6 +665,8 @@
|
|||
"bip47": {
|
||||
"payment_code": "支払コード",
|
||||
"contacts": "連絡先",
|
||||
"bip47_explain": "再利用・共有可能なコード",
|
||||
"bip47_explain_subtitle": "BIP47",
|
||||
"purpose": "再利用・共有可能なコード (BIP47)",
|
||||
"pay_this_contact": "この連絡先に支払う",
|
||||
"rename_contact": "連絡先をリネーム",
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
"ok": "ОК",
|
||||
"yes": "Иә",
|
||||
"no": "Жоқ",
|
||||
"save": "Сақтау",
|
||||
"close": "Жабу"
|
||||
},
|
||||
"entropy": {
|
||||
|
@ -54,7 +53,6 @@
|
|||
},
|
||||
"settings": {
|
||||
"about_license": "MIT License",
|
||||
"electrum_clear_alert_cancel": "Бас тарту",
|
||||
"header": "Баптаулар",
|
||||
"save": "Сақтау"
|
||||
},
|
||||
|
@ -65,7 +63,6 @@
|
|||
"wallets": {
|
||||
"add_create": "Жасау",
|
||||
"details_address": "Адрес",
|
||||
"details_no_cancel": "Жоқ, бас тарту",
|
||||
"import_do_import": "Еңгізу",
|
||||
"import_search_accounts": "Шот іздеу",
|
||||
"import_title": "Енгізу",
|
||||
|
|
|
@ -10,14 +10,12 @@
|
|||
"storage_is_encrypted": "ನಿಮ್ಮ ಸಂಗ್ರಹಣೆಯನ್ನು ಎನ್ಕ್ರಿಪ್ಟ್ ಮಾಡಲಾಗಿದೆ. ಅದನ್ನು ಡೀಕ್ರಿಪ್ಟ್ ಮಾಡಲು ಪಾಸ್ವರ್ಡ್ ಅಗತ್ಯವಿದೆ.",
|
||||
"yes": "ಹೌದು",
|
||||
"no": "ಇಲ್ಲ",
|
||||
"save": "ಉಳಿಸಿ",
|
||||
"wallet_key": "ವ್ಯಾಲೆಟ್ ಕೀ"
|
||||
},
|
||||
"entropy": {
|
||||
"save": "ಉಳಿಸಿ"
|
||||
},
|
||||
"settings": {
|
||||
"electrum_clear_alert_cancel": "ರದ್ದುಮಾಡಿ",
|
||||
"save": "ಉಳಿಸಿ"
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue