Merge branch 'staging' into tests/mainnet

This commit is contained in:
sneurlax 2022-11-07 08:40:57 -06:00
commit 65338ad87b
182 changed files with 24364 additions and 5383 deletions
.github/workflows
android
assets
crypto_plugins
ios/Runner.xcodeproj
lib
electrumx_rpc
hive
main.dart
models
pages
add_wallet_views
address_book_views
exchange_view
receive_view
send_view
settings_views
stack_privacy_calls.dart
wallet_view
wallets_view/sub_widgets
pages_desktop_specific

View file

@ -23,7 +23,7 @@ jobs:
run: |
cargo install cargo-ndk
rustup target add x86_64-unknown-linux-gnu
sudo apt install -y unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm
sudo apt install -y unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm
sudo apt install -y debhelper libclang-dev cargo rustc opencl-headers libssl-dev ocl-icd-opencl-dev
sudo apt install -y libc6-dev-i386
sudo apt install -y build-essential cmake git libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev pkg-config llvm
@ -50,48 +50,36 @@ jobs:
$encodedBytes = [System.Convert]::FromBase64String($env:CHANGE_NOW);
Set-Content $secretFileExchange -Value $encodedBytes -AsByteStream;
$secretFileExchangeHash = Get-FileHash $secretFileExchange;
Write-Output "::set-output name=SECRET_FILE_EXCHANGE::$secretFileExchange";
Write-Output "::set-output name=SECRET_FILE_EXCHANGE_HASH::$($secretFileExchangeHash.Hash)";
Write-Output "Secret file $secretFileExchange has hash $($secretFileExchangeHash.Hash)";
$secretFileBitcoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/bitcoin/bitcoin_wallet_test_parameters.dart";
$encodedBytes = [System.Convert]::FromBase64String($env:BITCOIN_TEST);
Set-Content $secretFileBitcoin -Value $encodedBytes -AsByteStream;
$secretFileBitcoinHash = Get-FileHash $secretFileBitcoin;
Write-Output "::set-output name=SECRET_FILE_BITCOIN::$secretFileBitcoin";
Write-Output "::set-output name=SECRET_FILE_BITCOIN_HASH::$($secretFileBitcoinHash.Hash)";
Write-Output "Secret file $secretFileBitcoin has hash $($secretFileBitcoinHash.Hash)";
$secretFileDogecoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/dogecoin/dogecoin_wallet_test_parameters.dart";
$encodedBytes = [System.Convert]::FromBase64String($env:DOGECOIN_TEST);
Set-Content $secretFileDogecoin -Value $encodedBytes -AsByteStream;
$secretFileDogecoinHash = Get-FileHash $secretFileDogecoin;
Write-Output "::set-output name=SECRET_FILE_DOGECOIN::$secretFileDogecoin";
Write-Output "::set-output name=SECRET_FILE_DOGECOIN_HASH::$($secretFileDogecoinHash.Hash)";
Write-Output "Secret file $secretFileDogecoin has hash $($secretFileDogecoinHash.Hash)";
$secretFileFiro = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/firo/firo_wallet_test_parameters.dart";
$encodedBytes = [System.Convert]::FromBase64String($env:FIRO_TEST);
Set-Content $secretFileFiro -Value $encodedBytes -AsByteStream;
$secretFileFiroHash = Get-FileHash $secretFileFiro;
Write-Output "::set-output name=SECRET_FILE_FIRO::$secretFileFiro";
Write-Output "::set-output name=SECRET_FILE_FIRO_HASH::$($secretFileFiroHash.Hash)";
Write-Output "Secret file $secretFileFiro has hash $($secretFileFiroHash.Hash)";
$secretFileBitcoinCash = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart";
$encodedBytes = [System.Convert]::FromBase64String($env:BITCOINCASH_TEST);
Set-Content $secretFileBitcoinCash -Value $encodedBytes -AsByteStream;
$secretFileBitcoinCashHash = Get-FileHash $secretFileBitcoinCash;
Write-Output "::set-output name=SECRET_FILE_BITCOINCASH::$secretFileBitcoinCash";
Write-Output "::set-output name=SECRET_FILE_BITCOINCASH_HASH::$($secretFileBitcoinCashHash.Hash)";
Write-Output "Secret file $secretFileBitcoinCash has hash $($secretFileBitcoinCashHash.Hash)";
$secretFileNamecoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/namecoin/namecoin_wallet_test_parameters.dart";
$encodedBytes = [System.Convert]::FromBase64String($env:NAMECOIN_TEST);
Set-Content $secretFileNamecoin -Value $encodedBytes -AsByteStream;
$secretFileNamecoinHash = Get-FileHash $secretFileNamecoin;
Write-Output "::set-output name=SECRET_FILE_NAMECOIN::$secretFileNamecoin";
Write-Output "::set-output name=SECRET_FILE_NAMECOIN_HASH::$($secretFileNamecoinHash.Hash)";
Write-Output "Secret file $secretFileNamecoin has hash $($secretFileNamecoinHash.Hash)";
shell: pwsh
@ -114,18 +102,18 @@ jobs:
file: coverage/lcov.info
- name: Delete temp files
run: |
Remove-Item -Path $env:CHANGE_NOW;
Remove-Item -Path $env:BITCOIN_TEST;
Remove-Item -Path $env:DOGECOIN_TEST;
Remove-Item -Path $env:FIRO_TEST;
Remove-Item -Path $env:BITCOINCASH_TEST;
Remove-Item -Path $env:NAMECOIN_TEST;
$secretFileExchange = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "lib/external_api_keys.dart";
$secretFileBitcoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/bitcoin/bitcoin_wallet_test_parameters.dart";
$secretFileDogecoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/dogecoin/dogecoin_wallet_test_parameters.dart";
$secretFileFiro = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/firo/firo_wallet_test_parameters.dart";
$secretFileBitcoinCash = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/bitcoincash/bitcoincash_wallet_test_parameters.dart";
$secretFileNamecoin = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "test/services/coins/namecoin/namecoin_wallet_test_parameters.dart";
Remove-Item -Path $secretFileExchange;
Remove-Item -Path $secretFileBitcoin;
Remove-Item -Path $secretFileDogecoin;
Remove-Item -Path $secretFileFiro;
Remove-Item -Path $secretFileBitcoinCash;
Remove-Item -Path $secretFileNamecoin;
shell: pwsh
if: always()
env:
CHANGE_NOW: ${{ steps.secret-file1.outputs.SECRET_FILE_EXCHANGE }}
BITCOIN_TEST: ${{ steps.secret-file1.outputs.SECRET_FILE_BITCOIN }}
DOGECOIN_TEST: ${{ steps.secret-file1.outputs.SECRET_FILE_DOGECOIN }}
FIRO_TEST: ${{ steps.secret-file1.outputs.SECRET_FILE_FIRO }}
BITCOINCASH_TEST: ${{ steps.secret-file1.outputs.SECRET_FILE_BITCOINCASH }}
NAMECOIN_TEST: ${{ steps.secret-file1.outputs.SECRET_FILE_NAMECOIN }}

View file

@ -20,6 +20,7 @@
<application
android:name="${applicationName}"
android:label="Stack Wallet"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher"
android:allowBackup="false"
android:fullBackupContent="false">

View file

@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.6.10'
ext.kotlin_version = '1.7.20'
repositories {
google()
jcenter()

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip

BIN
assets/images/litecoin.png Normal file

Binary file not shown.

After

(image error) Size: 322 KiB

6
assets/svg/Button.svg Normal file

File diff suppressed because one or more lines are too long

After

(image error) Size: 8 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.44444 2.21973C2.09618 2.21973 1 3.31591 1 4.66417V15.6642C1 17.0124 2.09618 18.1086 3.44444 18.1086H10.1667L9.75799 19.3308H7.11111C6.43507 19.3308 5.88889 19.877 5.88889 20.5531C5.88889 21.2291 6.43507 21.7753 7.11111 21.7753H16.8889C17.5649 21.7753 18.1111 21.2291 18.1111 20.5531C18.1111 19.877 17.5649 19.3308 16.8889 19.3308H14.242L13.8333 18.1086H20.5556C21.9038 18.1086 23 17.0124 23 15.6642V4.66417C23 3.31591 21.9038 2.21973 20.5556 2.21973H3.44444ZM20.5556 4.66417V13.2197H3.44444V4.66417H20.5556Z" fill="#232323"/>
</svg>

After

(image error) Size: 641 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.8242 1H4.44922C2.93027 1 1.69922 2.23105 1.69922 3.75V20.25C1.69922 21.7689 2.93027 23 4.44922 23H16.8242C18.3432 23 19.5742 21.7689 19.5742 20.25V3.75C19.5742 2.23105 18.341 1 16.8242 1ZM10.6367 6.5C12.1557 6.5 13.3867 7.73105 13.3867 9.25C13.3867 10.7689 12.1557 12 10.6367 12C9.1182 12 7.88672 10.7689 7.88672 9.25C7.88672 7.73105 9.11992 6.5 10.6367 6.5ZM14.7617 17.5H6.51172C6.13359 17.5 5.82422 17.1906 5.82422 16.8125C5.82422 14.9133 7.3625 13.375 9.26172 13.375H12.0117C13.9101 13.375 15.4492 14.9141 15.4492 16.8125C15.4492 17.1906 15.1398 17.5 14.7617 17.5ZM21.6367 3.75H20.9492V7.875H21.6367C22.0148 7.875 22.3242 7.56563 22.3242 7.1875V4.4375C22.3242 4.05766 22.0148 3.75 21.6367 3.75ZM21.6367 9.25H20.9492V13.375H21.6367C22.0148 13.375 22.3242 13.0656 22.3242 12.6875V9.9375C22.3242 9.55937 22.0148 9.25 21.6367 9.25ZM21.6367 14.75H20.9492V18.875H21.6367C22.0164 18.875 22.3242 18.5672 22.3242 18.1875V15.4375C22.3242 15.0594 22.0148 14.75 21.6367 14.75Z" fill="#232323"/>
</svg>

After

(image error) Size: 1.1 KiB

View file

@ -0,0 +1,4 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 4.16602V15.8327" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.3327 10L10.4993 15.8333L4.66602 10" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

(image error) Size: 346 B

View file

@ -0,0 +1,3 @@
<svg width="12" height="7" viewBox="0 0 12 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 6L6 1L1 6" stroke="#8E9192" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

(image error) Size: 208 B

View file

@ -0,0 +1,11 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6052_99642)">
<rect width="24" height="24" rx="12" fill="white"/>
<path d="M11.9976 0C9.62389 0 7.30353 0.703873 5.3299 2.02261C3.35627 3.34135 1.81802 5.21572 0.909655 7.4087C0.00129377 9.60167 -0.236375 12.0148 0.226704 14.3428C0.689782 16.6709 1.83281 18.8093 3.51124 20.4878C5.18968 22.1662 7.32813 23.3092 9.65618 23.7723C11.9842 24.2354 14.3973 23.9977 16.5903 23.0893C18.7833 22.181 20.6577 20.6427 21.9764 18.6691C23.2951 16.6955 23.999 14.3751 23.999 12.0015C23.999 10.4254 23.6886 8.86478 23.0854 7.4087C22.4823 5.95261 21.5983 4.62958 20.4839 3.51514C19.3694 2.40071 18.0464 1.51669 16.5903 0.913556C15.1342 0.310427 13.5736 0 11.9976 0V0ZM12.1921 12.3963L10.9437 16.6087H17.6209C17.674 16.6088 17.7263 16.6213 17.7738 16.6451C17.8212 16.669 17.8625 16.7035 17.8943 16.746C17.9261 16.7885 17.9476 16.8378 17.9571 16.8901C17.9666 16.9423 17.9638 16.9961 17.9489 17.0471L17.3683 19.0473C17.3406 19.1429 17.2826 19.2268 17.203 19.2865C17.1234 19.3462 17.0265 19.3784 16.927 19.3783H6.72841L8.45286 13.5546L6.54551 14.1352L6.96646 12.7737L8.87671 12.1931L11.2979 4.0121C11.3245 3.91623 11.3818 3.8317 11.4609 3.77142C11.5401 3.71114 11.6368 3.67842 11.7363 3.67824H14.32C14.3731 3.67805 14.4254 3.69017 14.4729 3.71364C14.5205 3.73712 14.5619 3.77131 14.594 3.81352C14.6261 3.85573 14.6479 3.90482 14.6578 3.95691C14.6677 4.009 14.6654 4.06267 14.651 4.11371L12.6188 11.0318L14.5262 10.4512L14.1168 11.836L12.1921 12.3963Z" fill="#315D9E"/>
</g>
<defs>
<clipPath id="clip0_6052_99642">
<rect width="24" height="24" rx="12" fill="white"/>
</clipPath>
</defs>
</svg>

After

(image error) Size: 1.7 KiB

View file

@ -0,0 +1,24 @@
<svg width="200" height="162" viewBox="0 0 200 162" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5863_29353)">
<rect width="200" height="162" rx="8" fill="#2A2D34"/>
<rect x="10" y="10" width="180" height="20" rx="2" fill="#444953"/>
<rect x="16" y="16" width="106" height="8" rx="1" fill="#7E8692"/>
<rect x="10" y="40" width="180" height="20" rx="2" fill="#333942"/>
<rect x="16" y="46" width="106" height="8" rx="1" fill="#575C63"/>
<rect x="10" y="62" width="180" height="20" rx="2" fill="#333942"/>
<rect x="16" y="68" width="106" height="8" rx="1" fill="#575C63"/>
<rect x="10" y="84" width="180" height="20" rx="2" fill="#333942"/>
<rect x="16" y="90" width="106" height="8" rx="1" fill="#575C63"/>
<rect x="10" y="106" width="180" height="20" rx="2" fill="#333942"/>
<rect x="16" y="112" width="106" height="8" rx="1" fill="#575C63"/>
<rect x="10" y="128" width="180" height="20" rx="2" fill="#333942"/>
<rect x="16" y="134" width="106" height="8" rx="1" fill="#575C63"/>
<rect x="10" y="150" width="180" height="20" rx="2" fill="#333942"/>
<rect x="16" y="156" width="106" height="8" rx="1" fill="#575C63"/>
</g>
<defs>
<clipPath id="clip0_5863_29353">
<rect width="200" height="162" rx="8" fill="white"/>
</clipPath>
</defs>
</svg>

After

(image error) Size: 1.2 KiB

View file

@ -0,0 +1,4 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="24" fill="#E0E3E3"/>
<path d="M23.659 10.0005C24.8164 10.0005 25.7475 10.9367 25.7475 12.1005V13.483C26.8701 13.623 27.9926 13.973 29.089 14.2792C30.2029 14.5855 30.8555 15.7405 30.5423 16.8605C30.2377 17.9805 29.089 18.6367 27.9752 18.3217C27.1485 18.0942 26.3218 17.8492 25.4777 17.683C24.2073 17.438 22.7279 17.5517 21.5358 18.0767C20.4654 18.5405 19.5865 19.6605 20.7961 20.4392C21.9448 21.183 23.3893 21.4805 24.6772 21.8567C26.1913 22.2855 28.1144 22.8367 29.5589 23.8255C31.4386 25.1205 32.3175 27.2205 31.8998 29.478C31.5082 31.6567 29.994 33.0917 28.184 33.8267C27.4357 34.133 26.609 34.2467 25.7475 34.4217V35.9005C25.7475 37.0642 24.8164 38.0005 23.659 38.0005C22.5017 38.0005 21.5706 37.0642 21.5706 35.9005L21.4923 34.2117C20.1696 33.8967 18.7947 33.4417 17.4372 32.9955C16.3407 32.628 15.749 31.438 16.1058 30.3442C16.4713 29.2417 17.5764 28.6467 18.7425 29.0055C20.0738 29.443 21.4313 29.968 22.8063 30.178C24.4509 30.423 25.7649 30.2742 26.6264 29.9242C27.7838 29.4605 28.332 28.078 27.2007 27.2905C26.0347 26.4942 24.5379 26.1792 23.2065 25.803C21.7446 25.383 19.9259 24.8667 18.551 23.983C16.6627 22.7667 15.7055 20.7367 16.1145 18.5055C16.4974 16.3792 18.142 14.9705 19.8737 14.218C20.4045 13.9905 20.9788 13.8067 21.4923 13.6667V12.1005C21.4923 10.9367 22.5017 10.0005 23.5807 10.0005H23.659Z" fill="#232323"/>
</svg>

After

(image error) Size: 1.4 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.69844 6.70024C9.61406 3.78461 14.325 3.77055 17.2594 6.65336L15.3281 8.57993C15.0047 8.90336 14.9109 9.38618 15.0844 9.80805C15.2578 10.2299 15.6703 10.5018 16.125 10.5018H21.7266H22.125C22.7484 10.5018 23.25 10.0002 23.25 9.3768V3.3768C23.25 2.92211 22.9781 2.50961 22.5563 2.33618C22.1344 2.16274 21.6516 2.25649 21.3281 2.57993L19.3781 4.52993C15.2719 0.475238 8.65781 0.489301 4.575 4.5768C3.43125 5.72055 2.60625 7.06586 2.1 8.50492C1.82344 9.28774 2.23594 10.1409 3.01406 10.4174C3.79219 10.694 4.65 10.2815 4.92656 9.50336C5.2875 8.48149 5.87344 7.52055 6.69844 6.70024ZM0.75 14.6268V14.9831V15.0159V20.6268C0.75 21.0815 1.02187 21.494 1.44375 21.6674C1.86562 21.8409 2.34844 21.7471 2.67188 21.4237L4.62187 19.4737C8.72812 23.5284 15.3422 23.5143 19.425 19.4268C20.5688 18.2831 21.3984 16.9377 21.9047 15.5034C22.1812 14.7206 21.7687 13.8674 20.9906 13.5909C20.2125 13.3143 19.3547 13.7268 19.0781 14.5049C18.7172 15.5268 18.1313 16.4877 17.3063 17.3081C14.3906 20.2237 9.67969 20.2377 6.74531 17.3549L8.67188 15.4237C8.99531 15.1002 9.08906 14.6174 8.91562 14.1956C8.74219 13.7737 8.32969 13.5018 7.875 13.5018H2.26875H2.23594H1.875C1.25156 13.5018 0.75 14.0034 0.75 14.6268Z" fill="#232323"/>
</svg>

After

(image error) Size: 1.3 KiB

View file

@ -0,0 +1,3 @@
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.875 3.12012C7.63555 3.12012 8.25 2.50566 8.25 1.74512C8.25 0.98457 7.63555 0.370117 6.875 0.370117H4.125C1.84766 0.370117 0 2.21777 0 4.49512V15.4951C0 17.7725 1.84766 19.6201 4.125 19.6201H6.875C7.63555 19.6201 8.25 19.0057 8.25 18.2451C8.25 17.4846 7.63555 16.8701 6.875 16.8701H4.125C3.36445 16.8701 2.75 16.2557 2.75 15.4951V4.49512C2.75 3.73457 3.36445 3.12012 4.125 3.12012H6.875ZM21.6777 10.7428C21.884 10.5494 22 10.2787 22 9.99512C22 9.71152 21.884 9.44082 21.6777 9.24746L15.4902 3.40371C15.1895 3.12012 14.7512 3.04277 14.373 3.20605C13.9949 3.36934 13.75 3.74316 13.75 4.15137V7.24512H8.25C7.48945 7.24512 6.875 7.85957 6.875 8.62012V11.3701C6.875 12.1307 7.48945 12.7451 8.25 12.7451H13.75V15.8389C13.75 16.2514 13.9949 16.6209 14.373 16.7842C14.7512 16.9475 15.1895 16.8701 15.4902 16.5865L21.6777 10.7428Z" fill="#232323"/>
</svg>

After

(image error) Size: 954 B

View file

@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="8" fill="#E0E3E3"/>
<path d="M26 8H12.5C10.843 8 9.5 9.34297 9.5 11V29C9.5 30.657 10.843 32 12.5 32H26C27.657 32 29 30.657 29 29V11C29 9.34297 27.6547 8 26 8ZM19.25 14C20.907 14 22.25 15.343 22.25 17C22.25 18.657 20.907 20 19.25 20C17.5934 20 16.25 18.657 16.25 17C16.25 15.343 17.5953 14 19.25 14ZM23.75 26H14.75C14.3375 26 14 25.6625 14 25.25C14 23.1781 15.6781 21.5 17.75 21.5H20.75C22.8209 21.5 24.5 23.1791 24.5 25.25C24.5 25.6625 24.1625 26 23.75 26ZM31.25 11H30.5V15.5H31.25C31.6625 15.5 32 15.1625 32 14.75V11.75C32 11.3356 31.6625 11 31.25 11ZM31.25 17H30.5V21.5H31.25C31.6625 21.5 32 21.1625 32 20.75V17.75C32 17.3375 31.6625 17 31.25 17ZM31.25 23H30.5V27.5H31.25C31.6642 27.5 32 27.1642 32 26.75V23.75C32 23.3375 31.6625 23 31.25 23Z" fill="#232323"/>
</svg>

After

(image error) Size: 899 B

View file

@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="8" fill="#E0E3E3"/>
<path d="M30.6765 16.1586C30.8183 16.5281 30.698 16.9449 30.4058 17.2156L28.5452 18.9086C28.5925 19.2652 28.6183 19.6305 28.6183 19.9613C28.6183 20.3695 28.5925 20.7348 28.5452 21.0914L30.4058 22.7844C30.698 23.0551 30.8183 23.4676 30.6765 23.8414C30.4874 24.3527 30.2597 24.8469 30.0019 25.3152L29.7999 25.6633C29.5163 26.1359 29.1984 26.5828 28.8503 27.0082C28.5925 27.3133 28.1757 27.4207 27.7976 27.3004L25.4042 26.5355C24.8284 26.9781 24.1538 27.3477 23.5136 27.6313L22.9765 30.0848C22.8906 30.4715 22.5898 30.7465 22.1945 30.8496C21.6015 30.9484 20.9913 31 20.3296 31C19.7452 31 19.1351 30.9484 18.5421 30.8496C18.1468 30.7465 17.846 30.4715 17.7601 30.0848L17.223 27.6313C16.5441 27.3477 15.9081 26.9781 15.3323 26.5355L12.9407 27.3004C12.5609 27.4207 12.1419 27.3133 11.8875 27.0082C11.5391 26.5828 11.2211 26.1359 10.9375 25.6633L10.7364 25.3152C10.4756 24.8469 10.2487 24.3527 10.0584 23.8414C9.91914 23.4719 10.0364 23.0551 10.3312 22.7844L12.19 21.0914C12.1428 20.7348 12.1183 20.3695 12.1183 20C12.1183 19.6305 12.1428 19.2652 12.19 18.9086L10.3312 17.2156C10.0364 16.9449 9.91914 16.5324 10.0584 16.1586C10.2487 15.6473 10.476 15.1531 10.7364 14.6848L10.9371 14.3367C11.2207 13.8641 11.5391 13.4172 11.8875 12.9939C12.1419 12.6867 12.5609 12.5802 12.9407 12.7013L15.3323 13.4645C15.9081 13.0202 16.5441 12.6506 17.223 12.37L17.7601 9.91652C17.846 9.52637 18.1468 9.21656 18.5421 9.15082C19.1351 9.05161 19.7452 9 20.3296 9C20.9913 9 21.6015 9.05161 22.1945 9.15082C22.5898 9.21656 22.8906 9.52637 22.9765 9.91652L23.5136 12.37C24.1538 12.6506 24.8284 13.0202 25.4042 13.4645L27.7976 12.7013C28.1757 12.5802 28.5925 12.6867 28.8503 12.9939C29.1984 13.4172 29.5163 13.8641 29.7999 14.3367L30.0019 14.6848C30.2597 15.1531 30.4874 15.6473 30.6765 16.1586ZM20.3683 23.4375C22.2675 23.4375 23.8058 21.8992 23.8058 19.9613C23.8058 18.1008 22.2675 16.5238 20.3683 16.5238C18.4691 16.5238 16.9308 18.1008 16.9308 19.9613C16.9308 21.8992 18.4691 23.4375 20.3683 23.4375Z" fill="#232323"/>
</svg>

After

(image error) Size: 2.1 KiB

23
assets/svg/keys.svg Normal file
View file

@ -0,0 +1,23 @@
<svg width="99" height="57" viewBox="0 0 99 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6055_8386)">
<path d="M90.1893 8.04883H18.9405C14.0708 8.04883 10.123 11.9965 10.123 16.8663V47.6738C10.123 52.5436 14.0708 56.4913 18.9405 56.4913H90.1893C95.0591 56.4913 99.0068 52.5436 99.0068 47.6738V16.8663C99.0068 11.9965 95.0591 8.04883 90.1893 8.04883Z" fill="#E1E2E3"/>
<path d="M91.9528 5.84445V44.9928C91.9528 47.7275 89.7366 49.9437 87.002 49.9437H5.85138C3.11082 49.9437 0.894531 47.7275 0.894531 44.9928V5.84445C0.894531 3.10984 3.11082 0.893555 5.85138 0.893555L88.2888 1.06037C90.4038 1.63232 91.9528 3.55667 91.9528 5.84445V5.84445Z" stroke="#222222" stroke-width="3" stroke-miterlimit="10"/>
<path d="M15.9121 18.4336V32.4045" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M9.86523 21.9248L21.9595 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M21.9595 21.9248L9.86523 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M36.252 18.4336V32.4045" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M30.2051 21.9248L42.2993 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M42.2993 21.9248L30.2051 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M56.5918 18.4336V32.4045" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M50.5449 21.9248L62.6392 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M62.6392 21.9248L50.5449 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M76.9316 18.4336V32.4045" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M70.8848 21.9248L82.979 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M82.979 21.9248L70.8848 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_6055_8386">
<rect width="99" height="56.4914" fill="white"/>
</clipPath>
</defs>
</svg>

After

(image error) Size: 2.3 KiB

View file

@ -0,0 +1,11 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="24" fill="#E0E3E3"/>
<g clip-path="url(#clip0_5784_28907)">
<path d="M29.602 20.0474C30.0832 20.0474 30.477 20.3954 30.477 20.9067V21.0786H33.102C33.5832 21.0786 33.977 21.4267 33.977 21.938C33.977 22.4106 33.5832 22.7974 33.102 22.7974H33.0145L32.9445 22.9907C32.5551 24.0048 31.9601 24.9931 31.2076 25.8009C31.247 25.8224 31.2863 25.8095 31.3257 25.8696L32.1526 26.3552C32.5682 26.6001 32.6995 27.1286 32.4501 27.5368C32.2051 27.945 31.667 28.0739 31.2513 27.829L30.4245 27.3435C30.232 27.2274 30.0001 27.1071 29.8513 26.9782C29.392 27.3005 28.8932 27.5798 28.3682 27.8118L28.2063 27.8806C27.7645 28.0739 27.2482 27.8763 27.0513 27.4423C26.8545 27.0083 27.0557 26.5013 27.4976 26.3079L27.6551 26.2392C27.9351 26.1146 28.2063 25.9384 28.4645 25.8181L27.9351 25.2938C27.5895 24.9587 27.5895 24.4173 27.9351 24.0821C28.2763 23.7427 28.8276 23.7427 29.1688 24.0821L29.8076 24.7052L29.8338 24.6923C30.3763 24.1681 30.8182 23.5149 31.1376 22.7587H26.452C25.9313 22.7587 25.577 22.4106 25.577 21.8993C25.577 21.4267 25.9313 21.0399 26.452 21.0399H28.727V20.8681C28.727 20.3954 29.0813 20.0087 29.602 20.0087V20.0474ZM17.002 23.0208L17.8332 24.8599H16.1313L17.002 23.0208ZM10.002 18.5005C10.002 16.9815 11.2554 15.7505 12.802 15.7505H35.202C36.7463 15.7505 38.002 16.9815 38.002 18.5005V29.5005C38.002 31.0173 36.7463 32.2505 35.202 32.2505H12.802C11.2554 32.2505 10.002 31.0173 10.002 29.5005V18.5005ZM24.002 29.5005H35.202V18.5005H24.002V29.5005ZM17.8026 20.5587C17.6626 20.2493 17.3476 20.0474 17.002 20.0474C16.6563 20.0474 16.3413 20.2493 16.2013 20.5587L13.4022 26.7462C13.2062 27.1415 13.4048 27.6872 13.8467 27.8806C14.2881 28.0739 14.8057 27.8763 15.0026 27.4423L15.392 26.5399H18.612L19.0013 27.4423C19.1982 27.8763 19.7145 28.0739 20.1563 27.8806C20.5982 27.6872 20.7995 27.1415 20.6026 26.7462L17.8026 20.5587Z" fill="#232323"/>
</g>
<defs>
<clipPath id="clip0_5784_28907">
<rect width="28" height="22" fill="white" transform="translate(10.002 13.0005)"/>
</clipPath>
</defs>
</svg>

After

(image error) Size: 2.1 KiB

View file

@ -0,0 +1,24 @@
<svg width="200" height="162" viewBox="0 0 200 162" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5887_94222)">
<rect width="200" height="162" rx="8" fill="#E8EAEC"/>
<rect x="10" y="10" width="180" height="20" rx="2" fill="#DBDDE1"/>
<rect x="16" y="16" width="106" height="8" rx="1" fill="#C4C8CC"/>
<rect x="10" y="40" width="180" height="20" rx="2" fill="#FEFEFE"/>
<rect x="16" y="46" width="106" height="8" rx="1" fill="#C4C8CC"/>
<rect x="10" y="62" width="180" height="20" rx="2" fill="#FEFEFE"/>
<rect x="16" y="68" width="106" height="8" rx="1" fill="#C4C8CC"/>
<rect x="10" y="84" width="180" height="20" rx="2" fill="#FEFEFE"/>
<rect x="16" y="90" width="106" height="8" rx="1" fill="#C4C8CC"/>
<rect x="10" y="106" width="180" height="20" rx="2" fill="#FEFEFE"/>
<rect x="16" y="112" width="106" height="8" rx="1" fill="#C4C8CC"/>
<rect x="10" y="128" width="180" height="20" rx="2" fill="#FEFEFE"/>
<rect x="16" y="134" width="106" height="8" rx="1" fill="#C4C8CC"/>
<rect x="10" y="150" width="180" height="20" rx="2" fill="#FEFEFE"/>
<rect x="16" y="156" width="106" height="8" rx="1" fill="#C4C8CC"/>
</g>
<defs>
<clipPath id="clip0_5887_94222">
<rect width="200" height="162" rx="8" fill="white"/>
</clipPath>
</defs>
</svg>

After

(image error) Size: 1.2 KiB

View file

@ -0,0 +1,11 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="24" fill="#E0E3E3"/>
<g clip-path="url(#clip0_5764_28774)">
<path d="M24 10.9995C28.2589 10.9995 31.7143 14.254 31.7143 18.2687V20.6918H32.5714C34.4625 20.6918 36 22.1406 36 23.9226V33.6149C36 35.3969 34.4625 36.8457 32.5714 36.8457H15.4286C13.5348 36.8457 12 35.3969 12 33.6149V23.9226C12 22.1406 13.5348 20.6918 15.4286 20.6918H16.2857V18.2687C16.2857 14.254 19.7411 10.9995 24 10.9995ZM24 14.2303C21.6321 14.2303 19.7143 16.0385 19.7143 18.2687V20.6918H28.2857V18.2687C28.2857 16.0385 26.3679 14.2303 24 14.2303ZM25.7143 27.1534C25.7143 26.2598 24.9482 25.538 24 25.538C23.0518 25.538 22.2857 26.2598 22.2857 27.1534V30.3841C22.2857 31.2776 23.0518 31.9995 24 31.9995C24.9482 31.9995 25.7143 31.2776 25.7143 30.3841V27.1534Z" fill="#232323"/>
</g>
<defs>
<clipPath id="clip0_5764_28774">
<rect width="24" height="25.8462" fill="white" transform="translate(12 10.9995)"/>
</clipPath>
</defs>
</svg>

After

(image error) Size: 1 KiB

View file

@ -0,0 +1,10 @@
<svg width="24" height="19" viewBox="0 0 24 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6129_18406)">
<path d="M14.9173 0C15.8835 0 16.6673 0.769499 16.6673 1.71875V5.15625C16.6673 6.10514 15.8835 6.875 14.9173 6.875H13.1673V8.02083H22.5007C23.146 8.02083 23.6673 8.53288 23.6673 9.16667C23.6673 9.80046 23.146 10.3125 22.5007 10.3125H19.0007V11.4583H20.7507C21.7168 11.4583 22.5007 12.2282 22.5007 13.1771V16.6146C22.5007 17.5635 21.7168 18.3333 20.7507 18.3333H14.9173C13.9512 18.3333 13.1673 17.5635 13.1673 16.6146V13.1771C13.1673 12.2282 13.9512 11.4583 14.9173 11.4583H16.6673V10.3125H7.33398V11.4583H9.08398C10.0501 11.4583 10.834 12.2282 10.834 13.1771V16.6146C10.834 17.5635 10.0501 18.3333 9.08398 18.3333H3.25065C2.28414 18.3333 1.50065 17.5635 1.50065 16.6146V13.1771C1.50065 12.2282 2.28414 11.4583 3.25065 11.4583H5.00065V10.3125H1.50065C0.856432 10.3125 0.333984 9.80046 0.333984 9.16667C0.333984 8.53288 0.856432 8.02083 1.50065 8.02083H10.834V6.875H9.08398C8.11784 6.875 7.33398 6.10514 7.33398 5.15625V1.71875C7.33398 0.769499 8.11784 0 9.08398 0H14.9173ZM9.66732 2.29167V4.58333H14.334V2.29167H9.66732ZM8.50065 16.0417V13.75H3.83398V16.0417H8.50065ZM15.5007 13.75V16.0417H20.1673V13.75H15.5007Z" fill="#232323"/>
</g>
<defs>
<clipPath id="clip0_6129_18406">
<rect width="23.3333" height="18.3333" fill="white" transform="translate(0.333984)"/>
</clipPath>
</defs>
</svg>

After

(image error) Size: 1.4 KiB

View file

@ -0,0 +1,4 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="24" fill="#E0E3E3"/>
<path d="M34.5 25.5H13.5C12.6741 25.5 12 26.1741 12 27V33C12 33.8259 12.6741 34.5 13.5 34.5H34.5C35.3259 34.5 36 33.8259 36 33V27C36 26.175 35.325 25.5 34.5 25.5ZM28.5 31.125C27.8789 31.125 27.375 30.6211 27.375 30C27.375 29.3789 27.8789 28.875 28.5 28.875C29.1211 28.875 29.625 29.3789 29.625 30C29.625 30.6211 29.1234 31.125 28.5 31.125ZM31.5 31.125C30.8789 31.125 30.375 30.6211 30.375 30C30.375 29.3789 30.8789 28.875 31.5 28.875C32.1211 28.875 32.625 29.3789 32.625 30C32.625 30.6211 32.1234 31.125 31.5 31.125ZM34.5 13.5H13.5C12.6741 13.5 12 14.1741 12 15V21C12 21.8259 12.6741 22.5 13.5 22.5H34.5C35.3259 22.5 36 21.8259 36 21V15C36 14.1741 35.325 13.5 34.5 13.5ZM28.5 19.125C27.8789 19.125 27.375 18.6211 27.375 18C27.375 17.3789 27.8813 16.875 28.5 16.875C29.1187 16.875 29.625 17.3812 29.625 18C29.625 18.6188 29.1234 19.125 28.5 19.125ZM31.5 19.125C30.8789 19.125 30.375 18.6211 30.375 18C30.375 17.3789 30.8813 16.875 31.5 16.875C32.1187 16.875 32.625 17.3812 32.625 18C32.625 18.6188 32.1234 19.125 31.5 19.125Z" fill="#232323"/>
</svg>

After

(image error) Size: 1.2 KiB

View file

@ -0,0 +1,4 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="24" fill="#E0E3E3"/>
<path d="M32.25 17.5031V15.75C32.25 14.9217 32.9203 14.25 33.75 14.25C34.5797 14.25 35.25 14.9217 35.25 15.75V21C35.25 21.8297 34.5797 22.5 33.75 22.5H32.5219C32.4984 22.5 32.475 22.5 32.4516 22.5H28.5C27.6703 22.5 27 21.8297 27 21C27 20.1703 27.6703 19.5 28.5 19.5H30C28.6313 17.6766 26.4516 16.5 23.9578 16.5C20.7375 16.5 17.9578 18.5859 16.9266 21.5016C16.6505 22.2797 15.7931 22.6922 15.0122 22.4156C14.2313 22.1391 13.8216 21.2391 14.0977 20.4984C15.5386 16.4241 19.425 13.5 23.9578 13.5C27.3469 13.5 30.2859 15.0666 32.25 17.5031ZM14.25 33.75C13.4217 33.75 12.75 33.0797 12.75 32.25V27C12.75 26.1703 13.4217 25.5 14.25 25.5H19.5C20.3297 25.5 21 26.1703 21 27C21 27.8297 20.3297 28.5 19.5 28.5H17.9578C19.3687 30.3234 21.5484 31.5 24 31.5C27.2625 31.5 30.0422 29.4141 31.0734 26.4984C31.35 25.7203 32.2078 25.3078 32.9859 25.5844C33.7688 25.8609 34.1766 26.7188 33.9 27.5016C32.4609 31.575 28.575 34.5 24 34.5C20.6531 34.5 17.6719 32.9344 15.75 30.4969V32.25C15.75 33.0797 15.0783 33.75 14.25 33.75Z" fill="#232323"/>
</svg>

After

(image error) Size: 1.2 KiB

11
assets/svg/sun-circle.svg Normal file
View file

@ -0,0 +1,11 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="24" fill="#E0E3E3"/>
<g clip-path="url(#clip0_5813_29015)">
<path d="M16.5734 18.4328C16.8289 18.6892 17.1657 18.8173 17.5015 18.8173C17.8373 18.8173 18.1758 18.6898 18.4328 18.4328C18.9455 17.9201 18.9455 17.0897 18.4328 16.5773L15.9555 14.0999C15.4445 13.5872 14.6123 13.5872 14.0994 14.0999C13.5864 14.6126 13.5867 15.443 14.0994 15.956L16.5734 18.4328ZM24 16.125C24.7246 16.125 25.3125 15.5371 25.3125 14.8125V11.3125C25.3125 10.5879 24.7273 10 24 10C23.2727 10 22.6875 10.5879 22.6875 11.3125V14.8125C22.6875 15.5398 23.2781 16.125 24 16.125ZM16.125 24C16.125 23.2754 15.5371 22.6875 14.8125 22.6875H11.3125C10.5879 22.6875 10 23.2781 10 24C10 24.7219 10.5879 25.3125 11.3125 25.3125H14.8125C15.5398 25.3125 16.125 24.7273 16.125 24ZM30.4969 18.8156C30.8327 18.8156 31.1695 18.6874 31.4249 18.4311L33.8995 15.9549C34.4122 15.4422 34.4122 14.6117 33.8995 14.0988C33.3868 13.5858 32.5548 13.5861 32.0434 14.0988L29.5688 16.575C29.0561 17.0877 29.0561 17.9181 29.5688 18.4306C29.8242 18.6898 30.1633 18.8156 30.4969 18.8156ZM24 31.875C23.2754 31.875 22.6875 32.4629 22.6875 33.1875V36.6875C22.6875 37.4148 23.2781 38 24 38C24.7219 38 25.3125 37.4121 25.3125 36.6875V33.1875C25.3125 32.4656 24.7273 31.875 24 31.875ZM16.5734 29.5672L14.0988 32.0434C13.5861 32.5561 13.5861 33.3866 14.0988 33.8995C14.3552 34.1559 14.6911 34.284 15.0269 34.284C15.3627 34.284 15.6995 34.1559 15.9549 33.8995L18.4295 31.4233C18.9422 30.9106 18.9422 30.0802 18.4295 29.5677C17.9168 29.0553 17.0875 29.0531 16.5734 29.5672ZM36.6875 22.6875H33.1875C32.4629 22.6875 31.875 23.2754 31.875 24C31.875 24.7246 32.4629 25.3125 33.1875 25.3125H36.6875C37.4148 25.3125 38 24.7273 38 24C38 23.2727 37.4148 22.6875 36.6875 22.6875ZM31.4266 29.5672C30.9156 29.0545 30.0834 29.0547 29.5705 29.5674C29.0575 30.0801 29.0578 30.9105 29.5705 31.4229L32.0451 33.8992C32.3006 34.1555 32.6373 34.2837 32.9731 34.2837C33.3089 34.2837 33.6447 34.1555 33.9012 33.8992C34.4139 33.3865 34.4139 32.556 33.9012 32.0431L31.4266 29.5672ZM24 17.875C20.6148 17.875 17.875 20.6148 17.875 24C17.875 27.383 20.617 30.125 24 30.125C27.383 30.125 30.125 27.383 30.125 24C30.125 20.6148 27.3852 17.875 24 17.875Z" fill="#232323"/>
</g>
<defs>
<clipPath id="clip0_5813_29015">
<rect width="28" height="28" fill="white" transform="translate(10 10)"/>
</clipPath>
</defs>
</svg>

After

(image error) Size: 2.4 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.25 2.83008C21.0105 2.83008 21.625 3.4165 21.625 4.1396C21.625 4.8627 21.0105 5.44913 20.25 5.44913H4.4375C4.05766 5.44913 3.75 5.74377 3.75 6.10389C3.75 6.46401 4.05766 6.75865 4.4375 6.75865H20.25C21.7668 6.75865 23 7.93313 23 9.3777V18.5444C23 19.9889 21.7668 21.1634 20.25 21.1634H3.75C2.23105 21.1634 1 19.9889 1 18.5444V5.44913C1 4.00251 2.23105 2.83008 3.75 2.83008H20.25ZM18.875 15.2706C19.6355 15.2706 20.25 14.6854 20.25 13.961C20.25 13.2367 19.6355 12.6515 18.875 12.6515C18.1145 12.6515 17.5 13.2367 17.5 13.961C17.5 14.6854 18.1145 15.2706 18.875 15.2706Z" fill="#232323"/>
</svg>

After

(image error) Size: 702 B

@ -1 +1 @@
Subproject commit 51f74f05d465a92e0118cf7c2bcfb049df21af42
Subproject commit 277d922c3b1d637c1ccda25f51395c618d293015

View file

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
@ -255,6 +255,7 @@
"${BUILT_PRODUCTS_DIR}/cw_monero/cw_monero.framework",
"${BUILT_PRODUCTS_DIR}/cw_shared_external/cw_shared_external.framework",
"${BUILT_PRODUCTS_DIR}/cw_wownero/cw_wownero.framework",
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/devicelocale/devicelocale.framework",
"${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework",
"${BUILT_PRODUCTS_DIR}/flutter_libmonero/flutter_libmonero.framework",
@ -288,6 +289,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cw_monero.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cw_shared_external.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cw_wownero.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/devicelocale.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_libmonero.framework",
@ -454,7 +456,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 78;
CURRENT_PROJECT_VERSION = 83;
DEVELOPMENT_TEAM = 4DQKUWSG6C;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -508,7 +510,7 @@
"$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**",
"$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs",
);
MARKETING_VERSION = 1.5.8;
MARKETING_VERSION = 1.5.11;
ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -641,7 +643,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 78;
CURRENT_PROJECT_VERSION = 83;
DEVELOPMENT_TEAM = 4DQKUWSG6C;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -695,7 +697,7 @@
"$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**",
"$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs",
);
MARKETING_VERSION = 1.5.8;
MARKETING_VERSION = 1.5.11;
ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -720,7 +722,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 78;
CURRENT_PROJECT_VERSION = 83;
DEVELOPMENT_TEAM = 4DQKUWSG6C;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -774,7 +776,7 @@
"$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**",
"$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs",
);
MARKETING_VERSION = 1.5.8;
MARKETING_VERSION = 1.5.11;
ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet;
PRODUCT_NAME = "$(TARGET_NAME)";

View file

@ -1,9 +1,12 @@
import 'dart:convert';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:string_validator/string_validator.dart';
class CachedElectrumX {
final ElectrumX? electrumXClient;
@ -94,10 +97,32 @@ class CachedElectrumX {
// update set with new data
if (newSet["setHash"] != "" && set["setHash"] != newSet["setHash"]) {
set["setHash"] = newSet["setHash"];
set["blockHash"] = newSet["blockHash"];
set["setHash"] = !isHexadecimal(newSet["setHash"] as String)
? base64ToReverseHex(newSet["setHash"] as String)
: newSet["setHash"];
set["blockHash"] = !isHexadecimal(newSet["blockHash"] as String)
? base64ToHex(newSet["blockHash"] as String)
: newSet["blockHash"];
for (int i = (newSet["coins"] as List).length - 1; i >= 0; i--) {
set["coins"].insert(0, newSet["coins"][i]);
dynamic newCoin = newSet["coins"][i];
List translatedCoin = [];
translatedCoin.add(!isHexadecimal(newCoin[0] as String)
? base64ToHex(newCoin[0] as String)
: newCoin[0]);
translatedCoin.add(!isHexadecimal(newCoin[1] as String)
? base64ToReverseHex(newCoin[1] as String)
: newCoin[1]);
try {
translatedCoin.add(!isHexadecimal(newCoin[2] as String)
? base64ToHex(newCoin[2] as String)
: newCoin[2]);
} catch (e, s) {
translatedCoin.add(newCoin[2]);
}
translatedCoin.add(!isHexadecimal(newCoin[3] as String)
? base64ToReverseHex(newCoin[3] as String)
: newCoin[3]);
set["coins"].insert(0, translatedCoin);
}
// save set to db
await DB.instance.put<dynamic>(
@ -118,6 +143,17 @@ class CachedElectrumX {
}
}
String base64ToHex(String source) =>
base64Decode(LineSplitter.split(source).join())
.map((e) => e.toRadixString(16).padLeft(2, '0'))
.join();
String base64ToReverseHex(String source) =>
base64Decode(LineSplitter.split(source).join())
.reversed
.map((e) => e.toRadixString(16).padLeft(2, '0'))
.join();
/// Call electrumx getTransaction on a per coin basis, storing the result in local db if not already there.
///
/// ElectrumX api only called if the tx does not exist in local db
@ -189,7 +225,15 @@ class CachedElectrumX {
);
final serials = await client.getUsedCoinSerials(startNumber: startNumber);
cachedSerials.addAll(serials["serials"] as List);
List newSerials = [];
for (var element in (serials["serials"] as List)) {
if (!isHexadecimal(element as String)) {
newSerials.add(base64ToHex(element));
} else {
newSerials.add(element);
}
}
cachedSerials.addAll(newSerials);
await DB.instance.put<dynamic>(
boxName: DB.instance.boxNameUsedSerialsCache(coin: coin),

View file

@ -9,6 +9,7 @@ import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/notification_model.dart';
import 'package:stackwallet/models/trade_wallet_lookup.dart';
import 'package:stackwallet/services/wallets_service.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
@ -142,6 +143,17 @@ class DB {
_loadSharedCoinCacheBoxes(),
]);
_initialized = true;
try {
if (_boxPrefs.get("familiarity") == null) {
await _boxPrefs.put("familiarity", 0);
}
int count = _boxPrefs.get("familiarity") as int;
await _boxPrefs.put("familiarity", count + 1);
Constants.exchangeForExperiencedUsers(count + 1);
} catch (e, s) {
print("$e $s");
}
}
}

View file

@ -30,7 +30,8 @@ import 'package:stackwallet/pages/loading_view.dart';
import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart';
import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_login_view.dart';
import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart';
import 'package:stackwallet/providers/global/auto_swb_service_provider.dart';
import 'package:stackwallet/providers/global/base_currencies_provider.dart';
// import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart';
@ -68,6 +69,9 @@ final openedFromSWBFileStringStateProvider =
void main() async {
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
GoogleFonts.config.allowRuntimeFetching = false;
if (Platform.isIOS) {
Util.libraryPath = await getLibraryDirectory();
}
if (Util.isDesktop) {
setWindowTitle('Stack Wallet');
@ -143,7 +147,12 @@ void main() async {
boxName: DB.boxNameDBInfo, key: "hive_data_version") as int? ??
0;
if (dbVersion < Constants.currentHiveDbVersion) {
await DbVersionMigrator().migrate(dbVersion);
try {
await DbVersionMigrator().migrate(dbVersion);
} catch (e, s) {
Logging.instance.log("Cannot migrate database\n$e $s",
level: LogLevel.Error, printFullLength: true);
}
}
monero.onStartup();
@ -199,56 +208,67 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
late final Completer<void> loadingCompleter;
bool didLoad = false;
bool _desktopHasPassword = false;
Future<void> load() async {
if (didLoad) {
return;
}
didLoad = true;
await DB.instance.init();
await _prefs.init();
_notificationsService = ref.read(notificationsProvider);
_nodeService = ref.read(nodeServiceChangeNotifierProvider);
_tradesService = ref.read(tradesServiceProvider);
NotificationApi.prefs = _prefs;
NotificationApi.notificationsService = _notificationsService;
unawaited(ref.read(baseCurrenciesProvider).update());
await _nodeService.updateDefaults();
await _notificationsService.init(
nodeService: _nodeService,
tradesService: _tradesService,
prefs: _prefs,
);
ref.read(priceAnd24hChangeNotifierProvider).start(true);
await _wallets.load(_prefs);
loadingCompleter.complete();
// TODO: this should probably run unawaited. Keep commented out for now as proper community nodes ui hasn't been implemented yet
// unawaited(_nodeService.updateCommunityNodes());
// run without awaiting
if (Constants.enableExchange && _prefs.externalCalls) {
unawaited(ExchangeDataLoadingService().loadAll(ref));
}
if (_prefs.isAutoBackupEnabled) {
switch (_prefs.backupFrequencyType) {
case BackupFrequencyType.everyTenMinutes:
ref
.read(autoSWBServiceProvider)
.startPeriodicBackupTimer(duration: const Duration(minutes: 10));
break;
case BackupFrequencyType.everyAppStart:
unawaited(ref.read(autoSWBServiceProvider).doBackup());
break;
case BackupFrequencyType.afterClosingAWallet:
// ignore this case here
break;
try {
if (didLoad) {
return;
}
didLoad = true;
await DB.instance.init();
await _prefs.init();
if (Util.isDesktop) {
_desktopHasPassword =
await ref.read(storageCryptoHandlerProvider).hasPassword();
}
_notificationsService = ref.read(notificationsProvider);
_nodeService = ref.read(nodeServiceChangeNotifierProvider);
_tradesService = ref.read(tradesServiceProvider);
NotificationApi.prefs = _prefs;
NotificationApi.notificationsService = _notificationsService;
unawaited(ref.read(baseCurrenciesProvider).update());
await _nodeService.updateDefaults();
await _notificationsService.init(
nodeService: _nodeService,
tradesService: _tradesService,
prefs: _prefs,
);
ref.read(priceAnd24hChangeNotifierProvider).start(true);
await _wallets.load(_prefs);
loadingCompleter.complete();
// TODO: this should probably run unawaited. Keep commented out for now as proper community nodes ui hasn't been implemented yet
// unawaited(_nodeService.updateCommunityNodes());
// run without awaiting
if (Constants.enableExchange &&
_prefs.externalCalls &&
await _prefs.isExternalCallsSet()) {
unawaited(ExchangeDataLoadingService().loadAll(ref));
}
if (_prefs.isAutoBackupEnabled) {
switch (_prefs.backupFrequencyType) {
case BackupFrequencyType.everyTenMinutes:
ref.read(autoSWBServiceProvider).startPeriodicBackupTimer(
duration: const Duration(minutes: 10));
break;
case BackupFrequencyType.everyAppStart:
unawaited(ref.read(autoSWBServiceProvider).doBackup());
break;
case BackupFrequencyType.afterClosingAWallet:
// ignore this case here
break;
}
}
} catch (e, s) {
Logger.print("$e $s", normalLength: false);
}
}
@ -532,21 +552,23 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// FlutterNativeSplash.remove();
if (_wallets.hasWallets || _prefs.hasPin) {
// return HomeView();
if (Util.isDesktop &&
(_wallets.hasWallets || _desktopHasPassword)) {
String? startupWalletId;
if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) {
startupWalletId =
ref.read(prefsChangeNotifierProvider).startupWalletId;
}
// TODO proper desktop auth view
if (Util.isDesktop) {
Future<void>.delayed(Duration.zero).then((value) =>
Navigator.of(context).pushNamedAndRemoveUntil(
DesktopHomeView.routeName, (route) => false));
return Container();
return DesktopLoginView(startupWalletId: startupWalletId);
} else if (!Util.isDesktop &&
(_wallets.hasWallets || _prefs.hasPin)) {
// return HomeView();
String? startupWalletId;
if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) {
startupWalletId =
ref.read(prefsChangeNotifierProvider).startupWalletId;
}
return LockscreenView(

View file

@ -276,6 +276,12 @@ class ExchangeFormState extends ChangeNotifier {
void _onExchangeRateTypeChanged() {
print("_onExchangeRateTypeChanged");
updateRanges(shouldNotifyListeners: true).then(
(_) => updateEstimate(
shouldNotifyListeners: true,
reversed: reversed,
),
);
}
void _onExchangeTypeChanged() {

View file

@ -1,14 +1,16 @@
class TransactionFilter {
final bool sent;
final bool received;
final DateTime from;
final DateTime to;
final bool trade;
final DateTime? from;
final DateTime? to;
final int? amount;
final String keyword;
TransactionFilter({
required this.sent,
required this.received,
required this.trade,
required this.from,
required this.to,
required this.amount,
@ -18,6 +20,7 @@ class TransactionFilter {
TransactionFilter copyWith({
bool? sent,
bool? received,
bool? trade,
DateTime? from,
DateTime? to,
int? amount,
@ -26,6 +29,7 @@ class TransactionFilter {
return TransactionFilter(
sent: sent ?? this.sent,
received: received ?? this.received,
trade: trade ?? this.trade,
from: from ?? this.from,
to: to ?? this.to,
amount: amount ?? this.amount,
@ -35,6 +39,6 @@ class TransactionFilter {
@override
String toString() {
return "TxFilter { sent: $sent, received: $received, from: $from, to: $to, amount: $amount, keyword: $keyword }";
return "TxFilter { sent: $sent, received: $received, trade: $trade, from: $from, to: $to, amount: $amount, keyword: $keyword }";
}
}

View file

@ -90,6 +90,8 @@ class _AddWalletViewState extends State<AddWalletView> {
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _searchFieldController,
focusNode: _searchFocusNode,
onChanged: (value) {
@ -204,7 +206,6 @@ class _AddWalletViewState extends State<AddWalletView> {
Expanded(
child: MobileCoinList(
coins: coins,
isDesktop: false,
),
),
const SizedBox(

View file

@ -8,11 +8,9 @@ class MobileCoinList extends StatelessWidget {
const MobileCoinList({
Key? key,
required this.coins,
required this.isDesktop,
}) : super(key: key);
final List<Coin> coins;
final bool isDesktop;
@override
Widget build(BuildContext context) {

View file

@ -26,7 +26,8 @@ class SearchableCoinList extends ConsumerWidget {
e.name.toLowerCase().contains(lowercaseTerm));
}
if (!showTestNetCoins) {
_coins.removeWhere((e) => e.name.endsWith("TestNet"));
_coins.removeWhere(
(e) => e.name.endsWith("TestNet") || e == Coin.bitcoincashTestnet);
}
// remove firo testnet regardless
_coins.remove(Coin.firoTestNet);

View file

@ -194,6 +194,8 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
onChanged: (string) {
if (string.isEmpty) {
if (_nextEnabled) {

View file

@ -6,10 +6,12 @@ class MnemonicTable extends StatelessWidget {
Key? key,
required this.words,
required this.isDesktop,
this.itemBorderColor,
}) : super(key: key);
final List<String> words;
final bool isDesktop;
final Color? itemBorderColor;
@override
Widget build(BuildContext context) {
@ -40,6 +42,7 @@ class MnemonicTable extends StatelessWidget {
number: ++index,
word: words[index - 1],
isDesktop: isDesktop,
borderColor: itemBorderColor,
),
),
],
@ -61,6 +64,7 @@ class MnemonicTable extends StatelessWidget {
number: i + 1,
word: words[i],
isDesktop: isDesktop,
borderColor: itemBorderColor,
),
),
],

View file

@ -9,16 +9,19 @@ class MnemonicTableItem extends StatelessWidget {
required this.number,
required this.word,
required this.isDesktop,
this.borderColor,
}) : super(key: key);
final int number;
final String word;
final bool isDesktop;
final Color? borderColor;
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
return RoundedWhiteContainer(
borderColor: borderColor,
padding: isDesktop
? const EdgeInsets.symmetric(horizontal: 12, vertical: 9)
: const EdgeInsets.all(8),

View file

@ -144,6 +144,8 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
Future<void> chooseDate() async {
final height = MediaQuery.of(context).size.height;
final fetchedColor =
Theme.of(context).extension<StackColors>()!.accentColorDark;
// check and hide keyboard
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
@ -155,8 +157,7 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
initialDate: DateTime.now(),
height: height * 0.5,
theme: ThemeData(
primarySwatch: Util.createMaterialColor(
Theme.of(context).extension<StackColors>()!.accentColorDark),
primarySwatch: Util.createMaterialColor(fetchedColor),
),
//TODO pick a better initial date
// 2007 chosen as that is just before bitcoin launched
@ -272,6 +273,7 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
// if (!isDesktop)
RestoreFromDatePicker(
onTap: chooseDate,
controller: _dateController,
),
// if (isDesktop)

View file

@ -7,6 +7,8 @@ import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
class MobileMnemonicLengthSelector extends ConsumerWidget {
const MobileMnemonicLengthSelector({
Key? key,
@ -19,7 +21,9 @@ class MobileMnemonicLengthSelector extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return Stack(
children: [
const TextField(
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
// controller: _lengthController,
readOnly: true,
textInputAction: TextInputAction.none,

View file

@ -4,11 +4,17 @@ import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
class RestoreFromDatePicker extends StatefulWidget {
const RestoreFromDatePicker({Key? key, required this.onTap})
: super(key: key);
const RestoreFromDatePicker({
Key? key,
required this.onTap,
required this.controller,
}) : super(key: key);
final VoidCallback onTap;
final TextEditingController controller;
@override
State<RestoreFromDatePicker> createState() => _RestoreFromDatePickerState();
@ -21,22 +27,18 @@ class _RestoreFromDatePickerState extends State<RestoreFromDatePicker> {
@override
void initState() {
onTap = widget.onTap;
_dateController = TextEditingController();
_dateController = widget.controller;
super.initState();
}
@override
void dispose() {
_dateController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.transparent,
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
onTap: onTap,
controller: _dateController,
style: STextStyles.field(context),

View file

@ -21,6 +21,8 @@ import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:stackwallet/utilities/util.dart';
class AddressBookView extends ConsumerStatefulWidget {
const AddressBookView({Key? key, this.coin}) : super(key: key);
@ -198,6 +200,8 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> {
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _searchController,
focusNode: _searchFocusNode,
onChanged: (value) {

View file

@ -22,6 +22,8 @@ import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:stackwallet/utilities/util.dart';
class AddAddressBookEntryView extends ConsumerStatefulWidget {
const AddAddressBookEntryView({
Key? key,
@ -279,6 +281,8 @@ class _AddAddressBookEntryViewState
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: nameController,
focusNode: nameFocusNode,
style: STextStyles.field(context),

View file

@ -13,6 +13,8 @@ import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:stackwallet/utilities/util.dart';
class EditContactNameEmojiView extends ConsumerStatefulWidget {
const EditContactNameEmojiView({
Key? key,
@ -200,6 +202,8 @@ class _EditContactNameEmojiViewState
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: nameController,
focusNode: nameFocusNode,
style: STextStyles.field(context),

View file

@ -20,6 +20,8 @@ import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:stackwallet/utilities/util.dart';
class NewContactAddressEntryForm extends ConsumerStatefulWidget {
const NewContactAddressEntryForm({
Key? key,
@ -71,6 +73,8 @@ class _NewContactAddressEntryFormState
return Column(
children: [
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
readOnly: true,
style: STextStyles.field(context),
decoration: InputDecoration(
@ -154,6 +158,8 @@ class _NewContactAddressEntryFormState
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
focusNode: addressLabelFocusNode,
controller: addressLabelController,
style: STextStyles.field(context),
@ -197,6 +203,7 @@ class _NewContactAddressEntryFormState
Constants.size.circularBorderRadius,
),
child: TextField(
enableSuggestions: Util.isDesktop ? false : true,
focusNode: addressFocusNode,
controller: addressController,
style: STextStyles.field(context),
@ -324,7 +331,6 @@ class _NewContactAddressEntryFormState
key: const Key("addAddressBookEntryViewAddressField"),
readOnly: false,
autocorrect: false,
enableSuggestions: false,
// inputFormatters: <TextInputFormatter>[
// FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")),
// ],

View file

@ -9,6 +9,8 @@ import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:stackwallet/utilities/util.dart';
class EditTradeNoteView extends ConsumerStatefulWidget {
const EditTradeNoteView({
Key? key,
@ -85,6 +87,8 @@ class _EditNoteViewState extends ConsumerState<EditTradeNoteView> {
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _noteController,
style: STextStyles.field(context),
focusNode: noteFieldFocusNode,

View file

@ -16,6 +16,8 @@ import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:tuple/tuple.dart';
import 'package:stackwallet/utilities/util.dart';
class FixedRateMarketPairCoinSelectionView extends ConsumerStatefulWidget {
const FixedRateMarketPairCoinSelectionView({
Key? key,
@ -152,6 +154,8 @@ class _FixedRateMarketPairCoinSelectionViewState
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _searchController,
focusNode: _searchFocusNode,
onChanged: filter,

View file

@ -13,6 +13,8 @@ import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:stackwallet/utilities/util.dart';
class FloatingRateCurrencySelectionView extends StatefulWidget {
const FloatingRateCurrencySelectionView({
Key? key,
@ -108,6 +110,8 @@ class _FloatingRateCurrencySelectionViewState
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _searchController,
focusNode: _searchFocusNode,
onChanged: filter,

View file

@ -52,11 +52,13 @@ class _SendFromViewState extends ConsumerState<SendFromView> {
switch (coin) {
case Coin.bitcoin:
case Coin.bitcoincash:
case Coin.litecoin:
case Coin.dogecoin:
case Coin.epicCash:
case Coin.firo:
case Coin.namecoin:
case Coin.bitcoinTestNet:
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
case Coin.dogecoinTestNet:
case Coin.firoTestNet:

View file

@ -19,7 +19,11 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
@ -50,6 +54,10 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
late TextEditingController amountController;
late TextEditingController noteController;
late final bool isDesktop;
late String _uriString;
bool didGenerate = false;
final _amountFocusNode = FocusNode();
final _noteFocusNode = FocusNode();
@ -80,8 +88,151 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
}
}
String? _generateURI() {
final amountString = amountController.text;
final noteString = noteController.text;
if (amountString.isNotEmpty && Decimal.tryParse(amountString) == null) {
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Invalid amount",
context: context,
);
return null;
}
String query = "";
if (amountString.isNotEmpty) {
query += "amount=$amountString";
}
if (noteString.isNotEmpty) {
if (query.isNotEmpty) {
query += "&";
}
query += "message=$noteString";
}
final uri = Uri(
scheme: widget.coin.uriScheme,
host: widget.receivingAddress,
query: query.isNotEmpty ? query : null,
);
final uriString = uri.toString().replaceFirst("://", ":");
Logging.instance.log("Generated receiving QR code for: $uriString",
level: LogLevel.Info);
return uriString;
}
void onGeneratePressed() {
final uriString = _generateURI();
if (uriString == null) {
return;
}
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (_) {
final width = MediaQuery.of(context).size.width / 2;
return StackDialogBase(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
"New QR code",
style: STextStyles.pageTitleH2(context),
),
),
const SizedBox(
height: 12,
),
Center(
child: RepaintBoundary(
key: _qrKey,
child: SizedBox(
width: width + 20,
height: width + 20,
child: QrImage(
data: uriString,
size: width,
backgroundColor:
Theme.of(context).extension<StackColors>()!.popupBG,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
),
const SizedBox(
height: 12,
),
Center(
child: SizedBox(
width: width,
child: TextButton(
onPressed: () async {
// TODO: add save button as well
await _capturePng(true);
},
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(context),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: SvgPicture.asset(
Assets.svg.share,
width: 14,
height: 14,
),
),
const SizedBox(
width: 4,
),
Column(
children: [
Text(
"Share",
textAlign: TextAlign.center,
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
),
const SizedBox(
height: 2,
),
],
),
],
),
),
),
),
],
),
);
},
);
}
@override
void initState() {
isDesktop = Util.isDesktop;
_uriString = Uri(
scheme: widget.coin.uriScheme,
host: widget.receivingAddress,
).toString().replaceFirst("://", ":");
amountController = TextEditingController();
noteController = TextEditingController();
super.initState();
@ -100,311 +251,330 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 70));
}
if (mounted) {
Navigator.of(context).pop();
}
},
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 70));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Generate QR code",
style: STextStyles.navBarTitle(context),
),
),
title: Text(
"Generate QR code",
style: STextStyles.navBarTitle(context),
),
),
body: LayoutBuilder(
builder: (buildContext, constraints) {
return Padding(
padding: const EdgeInsets.only(
left: 12,
top: 12,
right: 12,
),
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 24,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
RoundedWhiteContainer(
child: Text(
"The new QR code with your address, amount and note will appear in the pop up window.",
style: STextStyles.itemSubtitle(context),
),
),
const SizedBox(
height: 12,
),
Text(
"Amount (Optional)",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
const SizedBox(
height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
controller: amountController,
focusNode: _amountFocusNode,
style: STextStyles.field(context),
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
onChanged: (_) => setState(() {}),
decoration: standardInputDecoration(
"Amount",
_amountFocusNode,
context,
).copyWith(
suffixIcon: amountController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
amountController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
const SizedBox(
height: 12,
),
Text(
"Note (Optional)",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
const SizedBox(
height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
controller: noteController,
focusNode: _noteFocusNode,
style: STextStyles.field(context),
onChanged: (_) => setState(() {}),
decoration: standardInputDecoration(
"Note",
_noteFocusNode,
context,
).copyWith(
suffixIcon: noteController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
noteController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
// SizedBox()
// Spacer(),
const SizedBox(
height: 8,
),
TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: () {
final amountString = amountController.text;
final noteString = noteController.text;
if (amountString.isNotEmpty &&
Decimal.tryParse(amountString) == null) {
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Invalid amount",
context: context,
);
return;
}
String query = "";
if (amountString.isNotEmpty) {
query += "amount=$amountString";
}
if (noteString.isNotEmpty) {
if (query.isNotEmpty) {
query += "&";
}
query += "message=$noteString";
}
final uri = Uri(
scheme: widget.coin.uriScheme,
host: widget.receivingAddress,
query: query.isNotEmpty ? query : null,
);
final uriString =
uri.toString().replaceFirst("://", ":");
Logging.instance.log(
"Generated receiving QR code for: $uriString",
level: LogLevel.Info);
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (_) {
final width =
MediaQuery.of(context).size.width / 2;
return StackDialogBase(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
"New QR code",
style:
STextStyles.pageTitleH2(context),
),
),
const SizedBox(
height: 12,
),
Center(
child: RepaintBoundary(
key: _qrKey,
child: SizedBox(
width: width + 20,
height: width + 20,
child: QrImage(
data: uriString,
size: width,
backgroundColor: Theme.of(
context)
.extension<StackColors>()!
.popupBG,
foregroundColor: Theme.of(
context)
.extension<StackColors>()!
.accentColorDark),
),
),
),
const SizedBox(
height: 12,
),
Center(
child: SizedBox(
width: width,
child: TextButton(
onPressed: () async {
// TODO: add save button as well
await _capturePng(true);
},
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(
context),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
Center(
child: SvgPicture.asset(
Assets.svg.share,
width: 14,
height: 14,
),
),
const SizedBox(
width: 4,
),
Column(
children: [
Text(
"Share",
textAlign:
TextAlign.center,
style: STextStyles.button(
context)
.copyWith(
color: Theme.of(context)
.extension<
StackColors>()!
.buttonTextSecondary,
),
),
const SizedBox(
height: 2,
),
],
),
],
),
),
),
),
],
),
);
},
);
},
child: Text(
"Generate QR Code",
style: STextStyles.button(context),
),
),
],
body: LayoutBuilder(
builder: (buildContext, constraints) {
return Padding(
padding: const EdgeInsets.only(
left: 12,
top: 12,
right: 12,
),
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 24,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(4),
child: child,
),
),
),
),
);
},
),
),
child: Padding(
padding: isDesktop
? const EdgeInsets.only(
top: 12,
left: 32,
right: 32,
bottom: 32,
)
: const EdgeInsets.all(0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max,
children: [
if (!isDesktop)
RoundedWhiteContainer(
child: Text(
"The new QR code with your address, amount and note will appear in the pop up window.",
style: STextStyles.itemSubtitle(context),
),
),
if (!isDesktop)
const SizedBox(
height: 12,
),
Text(
"Amount (Optional)",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconRight,
)
: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
);
},
SizedBox(
height: isDesktop ? 10 : 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: amountController,
focusNode: _amountFocusNode,
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultText,
height: 1.8,
)
: STextStyles.field(context),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
onChanged: (_) => setState(() {}),
decoration: standardInputDecoration(
"Amount",
_amountFocusNode,
context,
).copyWith(
contentPadding: isDesktop
? const EdgeInsets.only(
left: 16,
top: 11,
bottom: 12,
right: 5,
)
: null,
suffixIcon: amountController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
amountController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
SizedBox(
height: isDesktop ? 20 : 12,
),
Text(
"Note (Optional)",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconRight,
)
: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
SizedBox(
height: isDesktop ? 10 : 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: noteController,
focusNode: _noteFocusNode,
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultText,
height: 1.8,
)
: STextStyles.field(context),
onChanged: (_) => setState(() {}),
decoration: standardInputDecoration(
"Note",
_noteFocusNode,
context,
).copyWith(
contentPadding: isDesktop
? const EdgeInsets.only(
left: 16,
top: 11,
bottom: 12,
right: 5,
)
: null,
suffixIcon: noteController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
noteController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
SizedBox(
height: isDesktop ? 20 : 8,
),
PrimaryButton(
label: "Generate QR code",
onPressed: isDesktop
? () {
final uriString = _generateURI();
if (uriString == null) {
return;
}
setState(() {
didGenerate = true;
_uriString = uriString;
});
}
: onGeneratePressed,
desktopMed: true,
),
if (isDesktop && didGenerate)
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 20,
),
RoundedWhiteContainer(
borderColor: Theme.of(context)
.extension<StackColors>()!
.background,
child: Column(
children: [
Text(
"New QR Code",
style: STextStyles.desktopTextMedium(context),
),
const SizedBox(
height: 16,
),
Center(
child: RepaintBoundary(
key: _qrKey,
child: SizedBox(
width: 234,
height: 234,
child: QrImage(
data: _uriString,
size: 220,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.popupBG,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
),
const SizedBox(
height: 12,
),
Row(
children: [
SecondaryButton(
width: 170,
desktopMed: true,
onPressed: () async {
await _capturePng(false);
},
label: "Share",
icon: SvgPicture.asset(
Assets.svg.share,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
),
const SizedBox(
width: 16,
),
PrimaryButton(
width: 170,
desktopMed: true,
onPressed: () async {
// TODO: add save functionality instead of share
await _capturePng(true);
},
label: "Save",
icon: SvgPicture.asset(
Assets.svg.arrowDown,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
),
],
)
],
),
),
],
),
],
),
],
),
),
);
}

View file

@ -1,7 +1,9 @@
import 'dart:async';
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
import 'package:stackwallet/pages/send_view/sub_widgets/sending_transaction_dialog.dart';
@ -11,12 +13,17 @@ import 'package:stackwallet/providers/wallet/public_private_balance_state_provid
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
@ -45,6 +52,14 @@ class _ConfirmTransactionViewState
late final Map<String, dynamic> transactionInfo;
late final String walletId;
late final String routeOnSuccessName;
late final bool isDesktop;
int _fee = 12;
final List<int> _dropDownItems = [
12,
22,
234,
];
Future<void> _attemptSend(BuildContext context) async {
unawaited(showDialog<dynamic>(
@ -133,6 +148,7 @@ class _ConfirmTransactionViewState
@override
void initState() {
isDesktop = Util.isDesktop;
transactionInfo = widget.transactionInfo;
walletId = widget.walletId;
routeOnSuccessName = widget.routeOnSuccessName;
@ -143,234 +159,553 @@ class _ConfirmTransactionViewState
Widget build(BuildContext context) {
final managerProvider = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManagerProvider(walletId)));
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
leading: AppBarBackButton(
onPressed: () async {
// if (FocusScope.of(context).hasFocus) {
// FocusScope.of(context).unfocus();
// await Future<void>.delayed(Duration(milliseconds: 50));
// }
Navigator.of(context).pop();
},
),
title: Text(
"Confirm transaction",
style: STextStyles.navBarTitle(context),
),
),
body: LayoutBuilder(
builder: (builderContext, constraints) {
return Padding(
padding: const EdgeInsets.only(
left: 12,
top: 12,
right: 12,
),
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 24,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Send ${ref.watch(managerProvider.select((value) => value.coin)).ticker}",
style: STextStyles.pageTitleH1(context),
),
const SizedBox(
height: 12,
),
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Recipient",
style: STextStyles.smallMed12(context),
),
const SizedBox(
height: 4,
),
Text(
"${transactionInfo["address"] ?? "ERROR"}",
style: STextStyles.itemSubtitle12(context),
),
],
),
),
const SizedBox(
height: 12,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Amount",
style: STextStyles.smallMed12(context),
),
Text(
"${Format.satoshiAmountToPrettyString(
transactionInfo["recipientAmt"] as int,
ref.watch(
localeServiceChangeNotifierProvider
.select((value) => value.locale),
),
)} ${ref.watch(
managerProvider
.select((value) => value.coin),
).ticker}",
style: STextStyles.itemSubtitle12(context),
textAlign: TextAlign.right,
),
],
),
),
const SizedBox(
height: 12,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Transaction fee",
style: STextStyles.smallMed12(context),
),
Text(
"${Format.satoshiAmountToPrettyString(
transactionInfo["fee"] as int,
ref.watch(
localeServiceChangeNotifierProvider
.select((value) => value.locale),
),
)} ${ref.watch(
managerProvider
.select((value) => value.coin),
).ticker}",
style: STextStyles.itemSubtitle12(context),
textAlign: TextAlign.right,
),
],
),
),
const SizedBox(
height: 12,
),
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Note",
style: STextStyles.smallMed12(context),
),
const SizedBox(
height: 4,
),
Text(
transactionInfo["note"] as String,
style: STextStyles.itemSubtitle12(context),
),
],
),
),
const Spacer(),
const SizedBox(
height: 12,
),
RoundedContainer(
color: Theme.of(context)
.extension<StackColors>()!
.snackBarBackSuccess,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Total amount",
style:
STextStyles.titleBold12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textConfirmTotalAmount,
),
),
Text(
"${Format.satoshiAmountToPrettyString(
(transactionInfo["fee"] as int) +
(transactionInfo["recipientAmt"] as int),
ref.watch(
localeServiceChangeNotifierProvider
.select((value) => value.locale),
),
)} ${ref.watch(
managerProvider
.select((value) => value.coin),
).ticker}",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textConfirmTotalAmount,
),
textAlign: TextAlign.right,
),
],
),
),
const SizedBox(
height: 16,
),
TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: () async {
final unlocked = await Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
RouteGenerator.useMaterialPageRoute,
builder: (_) => const LockscreenView(
showBackButton: true,
popOnSuccess: true,
routeOnSuccessArguments: true,
routeOnSuccess: "",
biometricsCancelButtonString: "CANCEL",
biometricsLocalizedReason:
"Authenticate to send transaction",
biometricsAuthenticationTitle:
"Confirm Transaction",
),
settings: const RouteSettings(
name: "/confirmsendlockscreen"),
),
);
if (unlocked is bool && unlocked && mounted) {
unawaited(_attemptSend(context));
}
},
child: Text(
"Send",
style: STextStyles.button(context),
),
),
],
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
leading: AppBarBackButton(
onPressed: () async {
// if (FocusScope.of(context).hasFocus) {
// FocusScope.of(context).unfocus();
// await Future<void>.delayed(Duration(milliseconds: 50));
// }
Navigator.of(context).pop();
},
),
title: Text(
"Confirm transaction",
style: STextStyles.navBarTitle(context),
),
),
body: LayoutBuilder(
builder: (builderContext, constraints) {
return Padding(
padding: const EdgeInsets.only(
left: 12,
top: 12,
right: 12,
),
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 24,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(4),
child: child,
),
),
),
),
);
},
),
),
child: ConditionalParent(
condition: isDesktop,
builder: (child) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
AppBarBackButton(
size: 40,
iconSize: 24,
onPressed: () => Navigator.of(
context,
rootNavigator: true,
).pop(),
),
Text(
"Confirm ${ref.watch(managerProvider.select((value) => value.coin.ticker.toUpperCase()))} transaction",
style: STextStyles.desktopH3(context),
),
],
),
);
},
child,
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max,
children: [
if (!isDesktop)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Send ${ref.watch(managerProvider.select((value) => value.coin)).ticker}",
style: STextStyles.pageTitleH1(context),
),
const SizedBox(
height: 12,
),
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Recipient",
style: STextStyles.smallMed12(context),
),
const SizedBox(
height: 4,
),
Text(
"${transactionInfo["address"] ?? "ERROR"}",
style: STextStyles.itemSubtitle12(context),
),
],
),
),
const SizedBox(
height: 12,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Amount",
style: STextStyles.smallMed12(context),
),
Text(
"${Format.satoshiAmountToPrettyString(
transactionInfo["recipientAmt"] as int,
ref.watch(
localeServiceChangeNotifierProvider
.select((value) => value.locale),
),
)} ${ref.watch(
managerProvider.select((value) => value.coin),
).ticker}",
style: STextStyles.itemSubtitle12(context),
textAlign: TextAlign.right,
),
],
),
),
const SizedBox(
height: 12,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Transaction fee",
style: STextStyles.smallMed12(context),
),
Text(
"${Format.satoshiAmountToPrettyString(
transactionInfo["fee"] as int,
ref.watch(
localeServiceChangeNotifierProvider
.select((value) => value.locale),
),
)} ${ref.watch(
managerProvider.select((value) => value.coin),
).ticker}",
style: STextStyles.itemSubtitle12(context),
textAlign: TextAlign.right,
),
],
),
),
const SizedBox(
height: 12,
),
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Note",
style: STextStyles.smallMed12(context),
),
const SizedBox(
height: 4,
),
Text(
transactionInfo["note"] as String,
style: STextStyles.itemSubtitle12(context),
),
],
),
),
],
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(
top: 16,
left: 32,
right: 32,
bottom: 50,
),
child: RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
borderColor:
Theme.of(context).extension<StackColors>()!.background,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.background,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(
Constants.size.circularBorderRadius,
),
topRight: Radius.circular(
Constants.size.circularBorderRadius,
),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 22,
),
child: Row(
children: [
SvgPicture.asset(
Assets.svg.send(context),
width: 32,
height: 32,
),
const SizedBox(
width: 16,
),
Text(
"Send ${ref.watch(
managerProvider
.select((value) => value.coin),
).ticker}",
style: STextStyles.desktopTextMedium(context),
),
],
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Amount",
style: STextStyles.desktopTextExtraExtraSmall(
context),
),
const SizedBox(
height: 2,
),
Builder(
builder: (context) {
final amount =
transactionInfo["recipientAmt"] as int;
final coin = ref.watch(
managerProvider.select(
(value) => value.coin,
),
);
final externalCalls = ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.externalCalls));
String fiatAmount = "N/A";
if (externalCalls) {
final price = ref
.read(priceAnd24hChangeNotifierProvider)
.getPrice(coin)
.item1;
if (price > Decimal.zero) {
fiatAmount = Format.localizedStringAsFixed(
value: Format.satoshisToAmount(amount,
coin: coin) *
price,
locale: ref
.read(
localeServiceChangeNotifierProvider)
.locale,
decimalPlaces: 2,
);
}
}
return Row(
children: [
Text(
"${Format.satoshiAmountToPrettyString(
amount,
ref.watch(
localeServiceChangeNotifierProvider
.select((value) => value.locale),
),
)} ${coin.ticker}",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
if (externalCalls)
Text(
" | ",
style: STextStyles
.desktopTextExtraExtraSmall(
context),
),
if (externalCalls)
Text(
"~$fiatAmount ${ref.watch(prefsChangeNotifierProvider.select(
(value) => value.currency,
))}",
style: STextStyles
.desktopTextExtraExtraSmall(
context),
),
],
);
},
),
],
),
),
Container(
height: 1,
color: Theme.of(context)
.extension<StackColors>()!
.background,
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Send to",
style: STextStyles.desktopTextExtraExtraSmall(
context),
),
const SizedBox(
height: 2,
),
Text(
"${transactionInfo["address"] ?? "ERROR"}",
style: STextStyles.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
)
],
),
),
Container(
height: 1,
color: Theme.of(context)
.extension<StackColors>()!
.background,
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Note",
style: STextStyles.desktopTextExtraExtraSmall(
context),
),
const SizedBox(
height: 2,
),
Text(
transactionInfo["note"] as String,
style: STextStyles.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
)
],
),
),
],
),
),
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(
left: 32,
),
child: Text(
"Transaction fee (estimated)",
style: STextStyles.desktopTextExtraExtraSmall(context),
),
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(
top: 10,
left: 32,
right: 32,
),
child: DropdownButtonFormField(
value: _fee,
items: _dropDownItems
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(
e.toString(),
),
),
)
.toList(),
onChanged: (value) {
if (value is int) {
setState(() {
_fee = value;
});
}
},
),
),
if (!isDesktop) const Spacer(),
SizedBox(
height: isDesktop ? 23 : 12,
),
Padding(
padding: isDesktop
? const EdgeInsets.symmetric(
horizontal: 32,
)
: const EdgeInsets.all(0),
child: RoundedContainer(
padding: isDesktop
? const EdgeInsets.symmetric(
horizontal: 16,
vertical: 18,
)
: const EdgeInsets.all(12),
color: Theme.of(context)
.extension<StackColors>()!
.snackBarBackSuccess,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
isDesktop ? "Total amount to send" : "Total amount",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textConfirmTotalAmount,
)
: STextStyles.titleBold12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textConfirmTotalAmount,
),
),
Text(
"${Format.satoshiAmountToPrettyString(
(transactionInfo["fee"] as int) +
(transactionInfo["recipientAmt"] as int),
ref.watch(
localeServiceChangeNotifierProvider
.select((value) => value.locale),
),
)} ${ref.watch(
managerProvider.select((value) => value.coin),
).ticker}",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textConfirmTotalAmount,
)
: STextStyles.itemSubtitle12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textConfirmTotalAmount,
),
textAlign: TextAlign.right,
),
],
),
),
),
SizedBox(
height: isDesktop ? 28 : 16,
),
Padding(
padding: isDesktop
? const EdgeInsets.symmetric(
horizontal: 32,
)
: const EdgeInsets.all(0),
child: PrimaryButton(
label: "Send",
desktopMed: true,
onPressed: () async {
final unlocked = await Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
RouteGenerator.useMaterialPageRoute,
builder: (_) => const LockscreenView(
showBackButton: true,
popOnSuccess: true,
routeOnSuccessArguments: true,
routeOnSuccess: "",
biometricsCancelButtonString: "CANCEL",
biometricsLocalizedReason:
"Authenticate to send transaction",
biometricsAuthenticationTitle: "Confirm Transaction",
),
settings:
const RouteSettings(name: "/confirmsendlockscreen"),
),
);
if (unlocked is bool && unlocked && mounted) {
unawaited(_attemptSend(context));
}
},
),
),
if (isDesktop)
const SizedBox(
height: 32,
),
],
),
),
);
}

View file

@ -41,6 +41,8 @@ import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:stackwallet/utilities/util.dart';
class SendView extends ConsumerStatefulWidget {
const SendView({
Key? key,
@ -885,7 +887,10 @@ class _SendViewState extends ConsumerState<SendView> {
if (coin == Coin.firo)
Stack(
children: [
const TextField(
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions:
Util.isDesktop ? false : true,
readOnly: true,
textInputAction: TextInputAction.none,
),
@ -1061,6 +1066,8 @@ class _SendViewState extends ConsumerState<SendView> {
height: 8,
),
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
style: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
@ -1114,6 +1121,8 @@ class _SendViewState extends ConsumerState<SendView> {
),
if (Prefs.instance.externalCalls)
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
style: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
@ -1238,6 +1247,8 @@ class _SendViewState extends ConsumerState<SendView> {
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: noteController,
focusNode: _noteFocusNode,
style: STextStyles.field(context),
@ -1283,6 +1294,8 @@ class _SendViewState extends ConsumerState<SendView> {
Stack(
children: [
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: feeController,
readOnly: true,
textInputAction: TextInputAction.none,

View file

@ -3,6 +3,8 @@ import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class BuildingTransactionDialog extends StatefulWidget {
@ -50,37 +52,73 @@ class _RestoringDialogState extends State<BuildingTransactionDialog>
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
return false;
},
child: StackDialog(
title: "Generating transaction",
// // TODO get message from design team
// message: "<PLACEHOLDER>",
icon: RotationTransition(
turns: _spinAnimation,
child: SvgPicture.asset(
Assets.svg.arrowRotate,
color: Theme.of(context).extension<StackColors>()!.accentColorDark,
width: 24,
height: 24,
if (Util.isDesktop) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Generating transaction",
style: STextStyles.desktopH3(context),
),
const SizedBox(
height: 40,
),
RotationTransition(
turns: _spinAnimation,
child: SvgPicture.asset(
Assets.svg.arrowRotate,
color:
Theme.of(context).extension<StackColors>()!.accentColorDark,
width: 24,
height: 24,
),
),
const SizedBox(
height: 40,
),
SecondaryButton(
desktopMed: true,
label: "Cancel",
onPressed: () {
onCancel.call();
},
)
],
);
} else {
return WillPopScope(
onWillPop: () async {
return false;
},
child: StackDialog(
title: "Generating transaction",
// // TODO get message from design team
// message: "<PLACEHOLDER>",
icon: RotationTransition(
turns: _spinAnimation,
child: SvgPicture.asset(
Assets.svg.arrowRotate,
color:
Theme.of(context).extension<StackColors>()!.accentColorDark,
width: 24,
height: 24,
),
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(context),
child: Text(
"Cancel",
style: STextStyles.itemSubtitle12(context),
),
onPressed: () {
Navigator.of(context).pop();
onCancel.call();
},
),
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(context),
child: Text(
"Cancel",
style: STextStyles.itemSubtitle12(context),
),
onPressed: () {
Navigator.of(context).pop();
onCancel.call();
},
),
),
);
);
}
}
}

View file

@ -1,11 +1,15 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:event_bus/event_bus.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:stackwallet/models/isar/models/log.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
@ -24,6 +28,15 @@ import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:flutter_libepiccash/git_versions.dart' as EPIC_VERSIONS;
import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS;
import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS;
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/util.dart';
class DebugView extends ConsumerStatefulWidget {
const DebugView({Key? key}) : super(key: key);
@ -217,6 +230,8 @@ class _DebugViewState extends ConsumerState<DebugView> {
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _searchController,
focusNode: _searchFocusNode,
onChanged: (newString) {
@ -268,21 +283,77 @@ class _DebugViewState extends ConsumerState<DebugView> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// BlueTextButton(
// text: ref.watch(debugServiceProvider
// .select((value) => value.isPaused))
// ? "Unpause"
// : "Pause",
// onTap: () {
// ref
// .read(debugServiceProvider)
// .togglePauseUiUpdates();
// },
// ),
BlueTextButton(
text: "Save Debug Info to clipboard",
onTap: () async {
try {
final packageInfo =
await PackageInfo.fromPlatform();
final version = packageInfo.version;
final build = packageInfo.buildNumber;
final signature = packageInfo.buildSignature;
final appName = packageInfo.appName;
String firoCommit =
FIRO_VERSIONS.getPluginVersion();
String epicCashCommit =
EPIC_VERSIONS.getPluginVersion();
String moneroCommit =
MONERO_VERSIONS.getPluginVersion();
DeviceInfoPlugin deviceInfoPlugin =
DeviceInfoPlugin();
final deviceInfo =
await deviceInfoPlugin.deviceInfo;
var deviceInfoMap = deviceInfo.toMap();
deviceInfoMap.remove("systemFeatures");
final logs = filtered(
ref.watch(debugServiceProvider.select(
(value) => value.recentLogs)),
_searchTerm)
.reversed
.toList(growable: false);
List errorLogs = [];
for (var log in logs) {
if (log.logLevel == LogLevel.Error ||
log.logLevel == LogLevel.Fatal) {
errorLogs.add(
"${log.logLevel}: ${log.message}");
}
}
final finalDebugMap = {
"version": version,
"build": build,
"signature": signature,
"appName": appName,
"firoCommit": firoCommit,
"epicCashCommit": epicCashCommit,
"moneroCommit": moneroCommit,
"deviceInfoMap": deviceInfoMap,
"errorLogs": errorLogs,
};
Logging.instance.log(
json.encode(finalDebugMap),
level: LogLevel.Info,
printFullLength: true);
const ClipboardInterface clipboard =
ClipboardWrapper();
await clipboard.setData(
ClipboardData(
text: json.encode(finalDebugMap)),
);
} catch (e, s) {
Logging.instance
.log("$e $s", level: LogLevel.Error);
}
},
),
const Spacer(),
BlueTextButton(
text: "Save logs to file",
onTap: () async {
final systemfile = StackFileSystem();
await systemfile.prepareStorage();
Directory rootPath =
(await getApplicationDocumentsDirectory());
@ -309,8 +380,9 @@ class _DebugViewState extends ConsumerState<DebugView> {
} else {
path = await FilePicker.platform
.getDirectoryPath(
dialogTitle: "Choose Backup location",
initialDirectory: dir.path,
dialogTitle: "Choose Log Save Location",
initialDirectory:
systemfile.startPath!.path,
lockParentWindow: true,
);
}
@ -332,9 +404,17 @@ class _DebugViewState extends ConsumerState<DebugView> {
),
));
final filename = await ref
.read(debugServiceProvider)
.exportToFile(path, eventBus);
bool logssaved = true;
var filename;
try {
filename = await ref
.read(debugServiceProvider)
.exportToFile(path, eventBus);
} catch (e, s) {
logssaved = false;
Logging.instance
.log("$e $s", level: LogLevel.Error);
}
shouldPop = true;
@ -346,7 +426,9 @@ class _DebugViewState extends ConsumerState<DebugView> {
showDialog(
context: context,
builder: (context) => StackOkDialog(
title: "Logs saved to",
title: logssaved
? "Logs saved to"
: "Error Saving Logs",
message: "${path!}/$filename",
),
),
@ -356,7 +438,9 @@ class _DebugViewState extends ConsumerState<DebugView> {
showFloatingFlushBar(
type: FlushBarType.info,
context: context,
message: 'Logs file saved',
message: logssaved
? 'Logs file saved'
: "Error Saving Logs",
),
);
}

View file

@ -7,12 +7,18 @@ import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import '../../../widgets/rounded_white_container.dart';
class BaseCurrencySettingsView extends ConsumerStatefulWidget {
const BaseCurrencySettingsView({Key? key}) : super(key: key);
@ -100,31 +106,90 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> {
currenciesWithoutSelected.insert(0, current);
}
currenciesWithoutSelected = _filtered();
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Currency",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.only(
top: 12,
left: 16,
right: 16,
),
final isDesktop = Util.isDesktop;
return ConditionalParent(
condition: !isDesktop,
builder: (child) {
return Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Currency",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.only(
top: 12,
left: 16,
right: 16,
),
child: child,
),
);
},
child: ConditionalParent(
condition: isDesktop,
builder: (child) {
return Padding(
padding: const EdgeInsets.only(
top: 16,
bottom: 32,
left: 32,
right: 32,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: RoundedWhiteContainer(
padding: const EdgeInsets.all(20),
borderColor:
Theme.of(context).extension<StackColors>()!.background,
child: child,
),
),
const SizedBox(
height: 16,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
desktopMed: true,
onPressed: Navigator.of(context).pop,
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
label: "Save changes",
desktopMed: true,
onPressed: Navigator.of(context).pop,
),
),
],
),
],
),
);
},
child: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) {
@ -140,6 +205,8 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> {
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _searchController,
focusNode: _searchFocusNode,
onChanged: (newString) {

View file

@ -13,6 +13,8 @@ import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:stackwallet/utilities/util.dart';
class LanguageSettingsView extends ConsumerStatefulWidget {
const LanguageSettingsView({Key? key}) : super(key: key);
@ -138,6 +140,8 @@ class _LanguageViewState extends ConsumerState<LanguageSettingsView> {
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _searchController,
focusNode: _searchFocusNode,
onChanged: (newString) {

View file

@ -8,7 +8,6 @@ import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/global/node_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
@ -20,7 +19,12 @@ import 'package:stackwallet/utilities/test_epic_box_connection.dart';
import 'package:stackwallet/utilities/test_monero_node_connection.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
@ -57,6 +61,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
late final AddEditNodeViewType viewType;
late final Coin coin;
late final String? nodeId;
late final bool isDesktop;
late bool saveEnabled;
late bool testConnectionEnabled;
@ -115,10 +120,12 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
case Coin.bitcoin:
case Coin.bitcoincash:
case Coin.litecoin:
case Coin.dogecoin:
case Coin.firo:
case Coin.namecoin:
case Coin.bitcoinTestNet:
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
case Coin.firoTestNet:
case Coin.dogecoinTestNet:
@ -158,8 +165,198 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
return testPassed;
}
Future<void> attemptSave() async {
final canConnect = await _testConnection(showFlushBar: false);
bool? shouldSave;
if (!canConnect) {
await showDialog<dynamic>(
context: context,
useSafeArea: true,
barrierDismissible: true,
builder: (_) => isDesktop
? DesktopDialog(
maxWidth: 440,
maxHeight: 300,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
top: 32,
),
child: Row(
children: [
const SizedBox(
width: 32,
),
Text(
"Server currently unreachable",
style: STextStyles.desktopH3(context),
),
],
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
top: 16,
bottom: 32,
),
child: Column(
children: [
const Spacer(),
Text(
"Would you like to save this node anyways?",
style: STextStyles.desktopTextMedium(context),
),
const Spacer(
flex: 2,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
desktopMed: true,
onPressed: () => Navigator.of(
context,
rootNavigator: true,
).pop(false),
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
label: "Save",
desktopMed: true,
onPressed: () => Navigator.of(
context,
rootNavigator: true,
).pop(true),
),
),
],
),
],
),
),
),
],
),
)
: StackDialog(
title: "Server currently unreachable",
message: "Would you like to save this node anyways?",
leftButton: TextButton(
onPressed: () async {
Navigator.of(context).pop(false);
},
child: Text(
"Cancel",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
rightButton: TextButton(
onPressed: () async {
Navigator.of(context).pop(true);
},
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
child: Text(
"Save",
style: STextStyles.button(context),
),
),
),
).then((value) {
if (value is bool && value) {
shouldSave = true;
} else {
shouldSave = false;
}
});
}
if (!canConnect && !shouldSave!) {
// return without saving
return;
}
final formData = ref.read(nodeFormDataProvider);
// strip unused path
String address = formData.host!;
if (coin == Coin.monero || coin == Coin.wownero || coin == Coin.epicCash) {
if (address.startsWith("http")) {
final uri = Uri.parse(address);
address = "${uri.scheme}://${uri.host}";
}
}
switch (viewType) {
case AddEditNodeViewType.add:
NodeModel node = NodeModel(
host: address,
port: formData.port!,
name: formData.name!,
id: const Uuid().v1(),
useSSL: formData.useSSL!,
loginName: formData.login,
enabled: true,
coinName: coin.name,
isFailover: formData.isFailover!,
isDown: false,
);
await ref.read(nodeServiceChangeNotifierProvider).add(
node,
formData.password,
true,
);
if (mounted) {
Navigator.of(context)
.popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete));
}
break;
case AddEditNodeViewType.edit:
NodeModel node = NodeModel(
host: address,
port: formData.port!,
name: formData.name!,
id: nodeId!,
useSSL: formData.useSSL!,
loginName: formData.login,
enabled: true,
coinName: coin.name,
isFailover: formData.isFailover!,
isDown: false,
);
await ref.read(nodeServiceChangeNotifierProvider).add(
node,
formData.password,
true,
);
if (mounted) {
Navigator.of(context)
.popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete));
}
break;
}
}
@override
void initState() {
isDesktop = Util.isDesktop;
ref.refresh(nodeFormDataProvider);
viewType = widget.viewType;
@ -192,279 +389,203 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
.select((value) => value.getNodeById(id: nodeId!)))
: null;
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
viewType == AddEditNodeViewType.edit ? "Edit node" : "Add node",
style: STextStyles.navBarTitle(context),
),
actions: [
if (viewType == AddEditNodeViewType.edit)
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("deleteNodeAppBarButtonKey"),
size: 36,
shadows: const [],
color: Theme.of(context).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.trash,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
width: 20,
height: 20,
),
onPressed: () async {
Navigator.popUntil(context,
ModalRoute.withName(widget.routeOnSuccessOrDelete));
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
viewType == AddEditNodeViewType.edit ? "Edit node" : "Add node",
style: STextStyles.navBarTitle(context),
),
actions: [
if (viewType == AddEditNodeViewType.edit)
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("deleteNodeAppBarButtonKey"),
size: 36,
shadows: const [],
color:
Theme.of(context).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.trash,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
width: 20,
height: 20,
),
onPressed: () async {
Navigator.popUntil(context,
ModalRoute.withName(widget.routeOnSuccessOrDelete));
await ref.read(nodeServiceChangeNotifierProvider).delete(
nodeId!,
true,
);
},
await ref.read(nodeServiceChangeNotifierProvider).delete(
nodeId!,
true,
);
},
),
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.only(
top: 12,
left: 12,
right: 12,
bottom: 12,
],
),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(4),
child: ConstrainedBox(
constraints:
BoxConstraints(minHeight: constraints.maxHeight - 8),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
NodeForm(
node: node,
secureStore: widget.secureStore,
readOnly: false,
coin: widget.coin,
onChanged: (canSave, canTest) {
if (canSave != saveEnabled &&
canTest != testConnectionEnabled) {
setState(() {
saveEnabled = canSave;
testConnectionEnabled = canTest;
});
} else if (canSave != saveEnabled) {
setState(() {
saveEnabled = canSave;
});
} else if (canTest != testConnectionEnabled) {
setState(() {
testConnectionEnabled = canTest;
});
}
},
),
const Spacer(),
TextButton(
onPressed: testConnectionEnabled
? () async {
await _testConnection();
}
: null,
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(context),
child: Text(
"Test connection",
style: STextStyles.button(context).copyWith(
color: testConnectionEnabled
? Theme.of(context)
.extension<StackColors>()!
.textDark
: Theme.of(context)
.extension<StackColors>()!
.textWhite,
),
),
),
const SizedBox(height: 16),
TextButton(
style: saveEnabled
? Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context)
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonColor(context),
onPressed: saveEnabled
? () async {
final canConnect = await _testConnection(
showFlushBar: false);
bool? shouldSave;
if (!canConnect) {
await showDialog<dynamic>(
context: context,
useSafeArea: true,
barrierDismissible: true,
builder: (_) => StackDialog(
title: "Server currently unreachable",
message:
"Would you like to save this node anyways?",
leftButton: TextButton(
onPressed: () async {
Navigator.of(context).pop(false);
},
child: Text(
"Cancel",
style: STextStyles.button(context)
.copyWith(
color: Theme.of(context)
.extension<
StackColors>()!
.accentColorDark),
),
),
rightButton: TextButton(
onPressed: () async {
Navigator.of(context).pop(true);
},
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(
context),
child: Text(
"Save",
style: STextStyles.button(context),
),
),
),
).then((value) {
if (value is bool && value) {
shouldSave = true;
} else {
shouldSave = false;
}
});
}
if (!canConnect && !shouldSave!) {
// return without saving
return;
}
final formData =
ref.read(nodeFormDataProvider);
// strip unused path
String address = formData.host!;
if (coin == Coin.monero ||
coin == Coin.wownero ||
coin == Coin.epicCash) {
if (address.startsWith("http")) {
final uri = Uri.parse(address);
address = "${uri.scheme}://${uri.host}";
}
}
switch (viewType) {
case AddEditNodeViewType.add:
NodeModel node = NodeModel(
host: address,
port: formData.port!,
name: formData.name!,
id: const Uuid().v1(),
useSSL: formData.useSSL!,
loginName: formData.login,
enabled: true,
coinName: coin.name,
isFailover: formData.isFailover!,
isDown: false,
);
await ref
.read(
nodeServiceChangeNotifierProvider)
.add(
node,
formData.password,
true,
);
if (mounted) {
Navigator.of(context).popUntil(
ModalRoute.withName(
widget.routeOnSuccessOrDelete));
}
break;
case AddEditNodeViewType.edit:
NodeModel node = NodeModel(
host: address,
port: formData.port!,
name: formData.name!,
id: nodeId!,
useSSL: formData.useSSL!,
loginName: formData.login,
enabled: true,
coinName: coin.name,
isFailover: formData.isFailover!,
isDown: false,
);
await ref
.read(
nodeServiceChangeNotifierProvider)
.add(
node,
formData.password,
true,
);
if (mounted) {
Navigator.of(context).popUntil(
ModalRoute.withName(
widget.routeOnSuccessOrDelete));
}
break;
}
}
: null,
child: Text(
"Save",
style: STextStyles.button(context),
),
),
],
body: Padding(
padding: const EdgeInsets.only(
top: 12,
left: 12,
right: 12,
bottom: 12,
),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(4),
child: ConstrainedBox(
constraints:
BoxConstraints(minHeight: constraints.maxHeight - 8),
child: IntrinsicHeight(
child: child,
),
),
),
);
},
),
),
),
child: ConditionalParent(
condition: isDesktop,
builder: (child) => DesktopDialog(
maxWidth: 580,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
const SizedBox(
width: 8,
),
const AppBarBackButton(
iconSize: 24,
size: 40,
),
Text(
"Add new node",
style: STextStyles.desktopH3(context),
)
],
),
);
},
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
top: 16,
bottom: 32,
),
child: child,
),
],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
NodeForm(
node: node,
secureStore: widget.secureStore,
readOnly: false,
coin: widget.coin,
onChanged: (canSave, canTest) {
if (canSave != saveEnabled &&
canTest != testConnectionEnabled) {
setState(() {
saveEnabled = canSave;
testConnectionEnabled = canTest;
});
} else if (canSave != saveEnabled) {
setState(() {
saveEnabled = canSave;
});
} else if (canTest != testConnectionEnabled) {
setState(() {
testConnectionEnabled = canTest;
});
}
},
),
if (!isDesktop) const Spacer(),
if (isDesktop)
const SizedBox(
height: 78,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Test connection",
enabled: testConnectionEnabled,
desktopMed: true,
onPressed: testConnectionEnabled
? () async {
await _testConnection();
}
: null,
),
),
if (isDesktop)
const SizedBox(
width: 16,
),
if (isDesktop)
Expanded(
child: PrimaryButton(
label: "Save",
enabled: saveEnabled,
desktopMed: true,
onPressed: saveEnabled ? attemptSave : null,
),
),
],
),
if (!isDesktop)
const SizedBox(
height: 16,
),
if (!isDesktop)
TextButton(
style: saveEnabled
? Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context)
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonColor(context),
onPressed: saveEnabled ? attemptSave : null,
child: Text(
"Save",
style: STextStyles.button(context),
),
),
],
),
),
);
@ -529,11 +650,13 @@ class _NodeFormState extends ConsumerState<NodeForm> {
// TODO: which coin servers can have username and password?
switch (coin) {
case Coin.bitcoin:
case Coin.litecoin:
case Coin.dogecoin:
case Coin.firo:
case Coin.namecoin:
case Coin.bitcoincash:
case Coin.bitcoinTestNet:
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
case Coin.firoTestNet:
case Coin.dogecoinTestNet:
@ -648,6 +771,8 @@ class _NodeFormState extends ConsumerState<NodeForm> {
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
key: const Key("addCustomNodeNodeNameFieldKey"),
readOnly: widget.readOnly,
enabled: enableField(_nameController),
@ -695,6 +820,8 @@ class _NodeFormState extends ConsumerState<NodeForm> {
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
key: const Key("addCustomNodeNodeAddressFieldKey"),
readOnly: widget.readOnly,
enabled: enableField(_hostController),
@ -746,6 +873,8 @@ class _NodeFormState extends ConsumerState<NodeForm> {
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
key: const Key("addCustomNodeNodePortFieldKey"),
readOnly: widget.readOnly,
enabled: enableField(_portController),
@ -797,6 +926,8 @@ class _NodeFormState extends ConsumerState<NodeForm> {
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _usernameController,
readOnly: widget.readOnly,
enabled: enableField(_usernameController),
@ -844,6 +975,8 @@ class _NodeFormState extends ConsumerState<NodeForm> {
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _passwordController,
readOnly: widget.readOnly,
enabled: enableField(_passwordController),

View file

@ -7,18 +7,24 @@ import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:tuple/tuple.dart';
class CoinNodesView extends ConsumerStatefulWidget {
const CoinNodesView({
Key? key,
required this.coin,
this.rootNavigator = false,
}) : super(key: key);
static const String routeName = "/coinNodes";
final Coin coin;
final bool rootNavigator;
@override
ConsumerState<CoinNodesView> createState() => _CoinNodesViewState();
@ -37,69 +43,149 @@ class _CoinNodesViewState extends ConsumerState<CoinNodesView> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"${widget.coin.prettyName} nodes",
style: STextStyles.navBarTitle(context),
),
actions: [
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("manageNodesAddNewNodeButtonKey"),
size: 36,
shadows: const [],
color: Theme.of(context).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.plus,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
width: 20,
height: 20,
if (Util.isDesktop) {
return DesktopDialog(
child: Column(
children: [
Row(
children: [
const SizedBox(
width: 32,
),
onPressed: () {
Navigator.of(context).pushNamed(
AddEditNodeView.routeName,
arguments: Tuple4(
AddEditNodeViewType.add,
widget.coin,
null,
CoinNodesView.routeName,
SvgPicture.asset(
Assets.svg.iconFor(coin: widget.coin),
width: 24,
height: 24,
),
const SizedBox(
width: 12,
),
Text(
"${widget.coin.prettyName} nodes",
style: STextStyles.desktopH3(context),
textAlign: TextAlign.center,
),
Expanded(
child: DesktopDialogCloseButton(
onPressedOverride: Navigator.of(
context,
rootNavigator: widget.rootNavigator,
).pop,
),
),
],
),
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${widget.coin.prettyName} nodes",
style: STextStyles.desktopTextExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
),
);
},
textAlign: TextAlign.left,
),
BlueTextButton(
text: "Add new node",
onTap: () {
Navigator.of(context).pushNamed(
AddEditNodeView.routeName,
arguments: Tuple4(
AddEditNodeViewType.add,
widget.coin,
null,
CoinNodesView.routeName,
),
);
},
),
],
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.only(
top: 12,
left: 12,
right: 12,
const SizedBox(
width: 12,
),
Padding(
padding: const EdgeInsets.all(20),
child: NodesList(
coin: widget.coin,
popBackToRoute: CoinNodesView.routeName,
),
),
],
),
child: SingleChildScrollView(
child: NodesList(
coin: widget.coin,
popBackToRoute: CoinNodesView.routeName,
);
} else {
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"${widget.coin.prettyName} nodes",
style: STextStyles.navBarTitle(context),
),
actions: [
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("manageNodesAddNewNodeButtonKey"),
size: 36,
shadows: const [],
color: Theme.of(context).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.plus,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
width: 20,
height: 20,
),
onPressed: () {
Navigator.of(context).pushNamed(
AddEditNodeView.routeName,
arguments: Tuple4(
AddEditNodeViewType.add,
widget.coin,
null,
CoinNodesView.routeName,
),
);
},
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.only(
top: 12,
left: 12,
right: 12,
),
child: SingleChildScrollView(
child: NodesList(
coin: widget.coin,
popBackToRoute: CoinNodesView.routeName,
),
),
),
),
);
);
}
}
}

View file

@ -17,7 +17,13 @@ import 'package:stackwallet/utilities/test_epic_box_connection.dart';
import 'package:stackwallet/utilities/test_monero_node_connection.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/delete_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:tuple/tuple.dart';
class NodeDetailsView extends ConsumerStatefulWidget {
@ -48,6 +54,8 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
late final String nodeId;
late final String popRouteName;
bool _desktopReadOnly = true;
@override
initState() {
secureStore = widget.secureStore;
@ -98,6 +106,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
break;
case Coin.bitcoin:
case Coin.litecoin:
case Coin.dogecoin:
case Coin.firo:
case Coin.bitcoinTestNet:
@ -105,6 +114,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
case Coin.dogecoinTestNet:
case Coin.bitcoincash:
case Coin.namecoin:
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
final client = ElectrumX(
host: node!.host,
@ -124,130 +134,239 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
}
if (testPassed) {
showFloatingFlushBar(
type: FlushBarType.success,
message: "Server ping success",
context: context,
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "Server ping success",
context: context,
),
);
} else {
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Server unreachable",
context: context,
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Server unreachable",
context: context,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Node details",
style: STextStyles.navBarTitle(context),
),
actions: [
if (!nodeId.startsWith("default"))
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("nodeDetailsEditNodeAppBarButtonKey"),
size: 36,
shadows: const [],
color: Theme.of(context).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.pencil,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
width: 20,
height: 20,
final isDesktop = Util.isDesktop;
final node = ref.watch(nodeServiceChangeNotifierProvider
.select((value) => value.getNodeById(id: nodeId)));
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Node details",
style: STextStyles.navBarTitle(context),
),
actions: [
if (!nodeId.startsWith("default"))
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("nodeDetailsEditNodeAppBarButtonKey"),
size: 36,
shadows: const [],
color:
Theme.of(context).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.pencil,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
width: 20,
height: 20,
),
onPressed: () {
Navigator.of(context).pushNamed(
AddEditNodeView.routeName,
arguments: Tuple4(
AddEditNodeViewType.edit,
coin,
nodeId,
popRouteName,
),
);
},
),
onPressed: () {
Navigator.of(context).pushNamed(
AddEditNodeView.routeName,
arguments: Tuple4(
AddEditNodeViewType.edit,
coin,
nodeId,
popRouteName,
),
);
},
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.only(
top: 12,
left: 12,
right: 12,
],
),
child: LayoutBuilder(
builder: (context, constraints) {
final node = ref.watch(nodeServiceChangeNotifierProvider
.select((value) => value.getNodeById(id: nodeId)));
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(4),
child: ConstrainedBox(
constraints:
BoxConstraints(minHeight: constraints.maxHeight - 8),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
NodeForm(
node: node,
secureStore: secureStore,
readOnly: true,
coin: coin,
),
const Spacer(),
TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(context),
onPressed: () async {
await _testConnection(ref, context);
},
child: Text(
"Test connection",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
const SizedBox(height: 16),
],
body: Padding(
padding: const EdgeInsets.only(
top: 12,
left: 12,
right: 12,
),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(4),
child: ConstrainedBox(
constraints:
BoxConstraints(minHeight: constraints.maxHeight - 8),
child: IntrinsicHeight(
child: child,
),
),
),
);
},
),
),
),
child: ConditionalParent(
condition: isDesktop,
builder: (child) => DesktopDialog(
maxWidth: 580,
maxHeight: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
const SizedBox(
width: 8,
),
const AppBarBackButton(
iconSize: 24,
size: 40,
),
Text(
"Node details",
style: STextStyles.desktopH3(context),
)
],
),
);
},
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
top: 16,
bottom: 32,
),
child: child,
),
],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
NodeForm(
node: node,
secureStore: secureStore,
readOnly: isDesktop ? _desktopReadOnly : true,
coin: coin,
),
if (!isDesktop) const Spacer(),
if (isDesktop)
const SizedBox(
height: 22,
),
if (isDesktop)
SizedBox(
height: 56,
child: _desktopReadOnly
? null
: Row(
children: [
Expanded(
child: DeleteButton(
label: "Delete node",
desktopMed: true,
onPressed: () async {
Navigator.of(context).pop();
await ref
.read(nodeServiceChangeNotifierProvider)
.delete(
node!.id,
true,
);
},
),
),
const SizedBox(
width: 16,
),
const Spacer(),
],
),
),
if (isDesktop && !_desktopReadOnly)
const SizedBox(
height: 45,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Test connection",
desktopMed: true,
onPressed: () async {
await _testConnection(ref, context);
},
),
),
if (isDesktop)
const SizedBox(
width: 16,
),
if (isDesktop)
Expanded(
child: !nodeId.startsWith("default")
? PrimaryButton(
label: _desktopReadOnly ? "Edit" : "Save",
desktopMed: true,
onPressed: () async {
final shouldSave = _desktopReadOnly == false;
setState(() {
_desktopReadOnly = !_desktopReadOnly;
});
if (shouldSave) {
// todo save node
}
},
)
: Container(),
),
],
),
if (!isDesktop)
const SizedBox(
height: 16,
),
],
),
),
);

View file

@ -19,6 +19,8 @@ import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:stackwallet/utilities/util.dart';
class AutoBackupView extends ConsumerStatefulWidget {
const AutoBackupView({Key? key}) : super(key: key);
@ -423,6 +425,8 @@ class _AutoBackupViewState extends ConsumerState<AutoBackupView> {
height: 10,
),
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
key: const Key("backupFrequencyFieldKey"),
controller: frequencyController,
enabled: false,

View file

@ -27,6 +27,8 @@ import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:zxcvbn/zxcvbn.dart';
import 'package:stackwallet/utilities/util.dart';
class CreateAutoBackupView extends ConsumerStatefulWidget {
const CreateAutoBackupView({
Key? key,
@ -146,6 +148,8 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> {
),
if (!Platform.isAndroid)
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
onTap: Platform.isAndroid
? null
: () async {
@ -411,7 +415,9 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> {
),
Stack(
children: [
const TextField(
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
readOnly: true,
textInputAction: TextInputAction.none,
),

View file

@ -14,7 +14,11 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/progress_bar.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
@ -92,424 +96,460 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Create backup",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!Platform.isAndroid)
Consumer(builder: (context, ref, __) {
return Container(
color: Colors.transparent,
child: TextField(
onTap: Platform.isAndroid
? null
: () async {
try {
await stackFileSystem.prepareStorage();
final isDesktop = Util.isDesktop;
if (mounted) {
await stackFileSystem
.pickDir(context);
}
if (mounted) {
setState(() {
fileLocationController.text =
stackFileSystem.dirPath ?? "";
});
}
} catch (e, s) {
Logging.instance.log("$e\n$s",
level: LogLevel.Error);
}
},
controller: fileLocationController,
style: STextStyles.field(context),
decoration: InputDecoration(
hintText: "Save to...",
hintStyle: STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
SvgPicture.asset(
Assets.svg.folder,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
const SizedBox(
width: 12,
),
],
),
),
),
key: const Key(
"createBackupSaveToFileLocationTextFieldKey"),
readOnly: true,
toolbarOptions: const ToolbarOptions(
copy: true,
cut: false,
paste: false,
selectAll: false,
),
onChanged: (newValue) {
// ref.read(addressEntryDataProvider(widget.id)).address = newValue;
},
),
);
}),
if (!Platform.isAndroid)
const SizedBox(
height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("createBackupPasswordFieldKey1"),
focusNode: passwordFocusNode,
controller: passwordController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Create passphrase",
passwordFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"createBackupPasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
onChanged: (newValue) {
if (newValue.isEmpty) {
setState(() {
passwordFeedback = "";
});
return;
}
final result = zxcvbn.evaluate(newValue);
String suggestionsAndTips = "";
for (var sug
in result.feedback.suggestions!.toSet()) {
suggestionsAndTips += "$sug\n";
}
suggestionsAndTips += result.feedback.warning!;
String feedback =
// "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n"
suggestionsAndTips;
passwordStrength = result.score! / 4;
// hack fix to format back string returned from zxcvbn
if (feedback.contains("phrasesNo need")) {
feedback = feedback.replaceFirst(
"phrasesNo need", "phrases\nNo need");
}
if (feedback.endsWith("\n")) {
feedback =
feedback.substring(0, feedback.length - 2);
}
setState(() {
passwordFeedback = feedback;
});
},
),
),
if (passwordFocusNode.hasFocus ||
passwordRepeatFocusNode.hasFocus ||
passwordController.text.isNotEmpty)
Padding(
padding: EdgeInsets.only(
left: 12,
right: 12,
top: passwordFeedback.isNotEmpty ? 4 : 0,
),
child: passwordFeedback.isNotEmpty
? Text(
passwordFeedback,
style: STextStyles.infoSmall(context),
)
: null,
),
if (passwordFocusNode.hasFocus ||
passwordRepeatFocusNode.hasFocus ||
passwordController.text.isNotEmpty)
Padding(
padding: const EdgeInsets.only(
left: 12,
right: 12,
top: 10,
),
child: ProgressBar(
key: const Key("createStackBackUpProgressBar"),
width: MediaQuery.of(context).size.width - 32 - 24,
height: 5,
fillColor: passwordStrength < 0.51
? Theme.of(context)
.extension<StackColors>()!
.accentColorRed
: passwordStrength < 1
? Theme.of(context)
.extension<StackColors>()!
.accentColorYellow
: Theme.of(context)
.extension<StackColors>()!
.accentColorGreen,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
percent: passwordStrength < 0.25
? 0.03
: passwordStrength,
),
),
const SizedBox(
height: 10,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("createBackupPasswordFieldKey2"),
focusNode: passwordRepeatFocusNode,
controller: passwordRepeatController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Confirm passphrase",
passwordRepeatFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"createBackupPasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
onChanged: (newValue) {
setState(() {});
// TODO: ? check if passwords match?
},
),
),
const SizedBox(
height: 16,
),
const Spacer(),
TextButton(
style: shouldEnableCreate
? Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context)
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonColor(context),
onPressed: !shouldEnableCreate
? null
: () async {
final String pathToSave =
fileLocationController.text;
final String passphrase =
passwordController.text;
final String repeatPassphrase =
passwordRepeatController.text;
if (pathToSave.isEmpty) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "Directory not chosen",
context: context,
));
return;
}
if (!(await Directory(pathToSave).exists())) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "Directory does not exist",
context: context,
));
return;
}
if (passphrase.isEmpty) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "A passphrase is required",
context: context,
));
return;
}
if (passphrase != repeatPassphrase) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "Passphrase does not match",
context: context,
));
return;
}
unawaited(showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => const StackDialog(
title: "Encrypting backup",
message: "This shouldn't take long",
),
));
// make sure the dialog is able to be displayed for at least 1 second
await Future<void>.delayed(
const Duration(seconds: 1));
final DateTime now = DateTime.now();
final String fileToSave =
"$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb";
final backup =
await SWB.createStackWalletJSON();
bool result =
await SWB.encryptStackWalletWithPassphrase(
fileToSave,
passphrase,
jsonEncode(backup),
);
if (mounted) {
// pop encryption progress dialog
Navigator.of(context).pop();
if (result) {
await showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => Platform.isAndroid
? StackOkDialog(
title: "Backup saved to:",
message: fileToSave,
)
: const StackOkDialog(
title:
"Backup creation succeeded"),
);
passwordController.text = "";
passwordRepeatController.text = "";
setState(() {});
} else {
await showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => const StackOkDialog(
title: "Backup creation failed"),
);
}
}
},
child: Text(
"Create backup",
style: STextStyles.button(context),
),
),
],
return ConditionalParent(
condition: !isDesktop,
builder: (child) {
return Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Create backup",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: child,
),
),
);
},
),
),
);
},
child: ConditionalParent(
condition: isDesktop,
builder: (child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
"Choose file location",
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
),
),
);
},
// child,
const SizedBox(height: 20),
Row(
children: [
PrimaryButton(
desktopMed: true,
width: 200,
label: "Create backup",
onPressed: () {},
),
const SizedBox(width: 16),
SecondaryButton(
desktopMed: true,
width: 200,
label: "Cancel",
onPressed: () {},
),
],
),
],
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!Platform.isAndroid)
Consumer(builder: (context, ref, __) {
return Container(
color: Colors.transparent,
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
onTap: Platform.isAndroid
? null
: () async {
try {
await stackFileSystem.prepareStorage();
if (mounted) {
await stackFileSystem.pickDir(context);
}
if (mounted) {
setState(() {
fileLocationController.text =
stackFileSystem.dirPath ?? "";
});
}
} catch (e, s) {
Logging.instance
.log("$e\n$s", level: LogLevel.Error);
}
},
controller: fileLocationController,
style: STextStyles.field(context),
decoration: InputDecoration(
hintText: "Save to...",
hintStyle: STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
SvgPicture.asset(
Assets.svg.folder,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
const SizedBox(
width: 12,
),
],
),
),
),
key:
const Key("createBackupSaveToFileLocationTextFieldKey"),
readOnly: true,
toolbarOptions: const ToolbarOptions(
copy: true,
cut: false,
paste: false,
selectAll: false,
),
onChanged: (newValue) {
// ref.read(addressEntryDataProvider(widget.id)).address = newValue;
},
),
);
}),
if (!Platform.isAndroid)
const SizedBox(
height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("createBackupPasswordFieldKey1"),
focusNode: passwordFocusNode,
controller: passwordController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Create passphrase",
passwordFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"createBackupPasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: SvgPicture.asset(
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
onChanged: (newValue) {
if (newValue.isEmpty) {
setState(() {
passwordFeedback = "";
});
return;
}
final result = zxcvbn.evaluate(newValue);
String suggestionsAndTips = "";
for (var sug in result.feedback.suggestions!.toSet()) {
suggestionsAndTips += "$sug\n";
}
suggestionsAndTips += result.feedback.warning!;
String feedback =
// "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n"
suggestionsAndTips;
passwordStrength = result.score! / 4;
// hack fix to format back string returned from zxcvbn
if (feedback.contains("phrasesNo need")) {
feedback = feedback.replaceFirst(
"phrasesNo need", "phrases\nNo need");
}
if (feedback.endsWith("\n")) {
feedback = feedback.substring(0, feedback.length - 2);
}
setState(() {
passwordFeedback = feedback;
});
},
),
),
if (passwordFocusNode.hasFocus ||
passwordRepeatFocusNode.hasFocus ||
passwordController.text.isNotEmpty)
Padding(
padding: EdgeInsets.only(
left: 12,
right: 12,
top: passwordFeedback.isNotEmpty ? 4 : 0,
),
child: passwordFeedback.isNotEmpty
? Text(
passwordFeedback,
style: STextStyles.infoSmall(context),
)
: null,
),
if (passwordFocusNode.hasFocus ||
passwordRepeatFocusNode.hasFocus ||
passwordController.text.isNotEmpty)
Padding(
padding: const EdgeInsets.only(
left: 12,
right: 12,
top: 10,
),
child: ProgressBar(
key: const Key("createStackBackUpProgressBar"),
width: MediaQuery.of(context).size.width - 32 - 24,
height: 5,
fillColor: passwordStrength < 0.51
? Theme.of(context)
.extension<StackColors>()!
.accentColorRed
: passwordStrength < 1
? Theme.of(context)
.extension<StackColors>()!
.accentColorYellow
: Theme.of(context)
.extension<StackColors>()!
.accentColorGreen,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
percent: passwordStrength < 0.25 ? 0.03 : passwordStrength,
),
),
const SizedBox(
height: 10,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("createBackupPasswordFieldKey2"),
focusNode: passwordRepeatFocusNode,
controller: passwordRepeatController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Confirm passphrase",
passwordRepeatFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"createBackupPasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: SvgPicture.asset(
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
onChanged: (newValue) {
setState(() {});
// TODO: ? check if passwords match?
},
),
),
const SizedBox(
height: 16,
),
const Spacer(),
TextButton(
style: shouldEnableCreate
? Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context)
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonColor(context),
onPressed: !shouldEnableCreate
? null
: () async {
final String pathToSave = fileLocationController.text;
final String passphrase = passwordController.text;
final String repeatPassphrase =
passwordRepeatController.text;
if (pathToSave.isEmpty) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "Directory not chosen",
context: context,
));
return;
}
if (!(await Directory(pathToSave).exists())) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "Directory does not exist",
context: context,
));
return;
}
if (passphrase.isEmpty) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "A passphrase is required",
context: context,
));
return;
}
if (passphrase != repeatPassphrase) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "Passphrase does not match",
context: context,
));
return;
}
unawaited(showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => const StackDialog(
title: "Encrypting backup",
message: "This shouldn't take long",
),
));
// make sure the dialog is able to be displayed for at least 1 second
await Future<void>.delayed(const Duration(seconds: 1));
final DateTime now = DateTime.now();
final String fileToSave =
"$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb";
final backup = await SWB.createStackWalletJSON();
bool result = await SWB.encryptStackWalletWithPassphrase(
fileToSave,
passphrase,
jsonEncode(backup),
);
if (mounted) {
// pop encryption progress dialog
Navigator.of(context).pop();
if (result) {
await showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => Platform.isAndroid
? StackOkDialog(
title: "Backup saved to:",
message: fileToSave,
)
: const StackOkDialog(
title: "Backup creation succeeded"),
);
passwordController.text = "";
passwordRepeatController.text = "";
setState(() {});
} else {
await showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => const StackOkDialog(
title: "Backup creation failed"),
);
}
}
},
child: Text(
"Create backup",
style: STextStyles.button(context),
),
),
],
),
),
);

View file

@ -27,6 +27,8 @@ import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:zxcvbn/zxcvbn.dart';
import '../../../../utilities/util.dart';
class EditAutoBackupView extends ConsumerStatefulWidget {
const EditAutoBackupView({
Key? key,
@ -148,6 +150,8 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> {
),
if (!Platform.isAndroid)
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
onTap: Platform.isAndroid
? null
: () async {
@ -413,7 +417,9 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> {
),
Stack(
children: [
const TextField(
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
readOnly: true,
textInputAction: TextInputAction.none,
),

View file

@ -687,27 +687,14 @@ abstract class SWB {
uiState?.walletStates = walletStates;
List<Future<bool>> restoreStatuses = [];
final List<Tuple2<dynamic, Manager>> firoWallets = [];
final List<Tuple2<dynamic, Manager>> firoTestnetWallets = [];
final List<Tuple2<dynamic, Manager>> epicCashWallets = [];
// start restoring wallets
for (final tuple in managers) {
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
if (tuple.item2.coin == Coin.firoTestNet) {
firoTestnetWallets.add(tuple);
continue;
} else if (tuple.item2.coin == Coin.firo) {
firoWallets.add(tuple);
continue;
} else if (tuple.item2.coin == Coin.epicCash) {
epicCashWallets.add(tuple);
continue;
}
restoreStatuses.add(asyncRestore(tuple, uiState, walletsService));
final bools = await asyncRestore(tuple, uiState, walletsService);
restoreStatuses.add(Future(() => bools));
}
// check if cancel was requested and restore previous state
@ -715,153 +702,6 @@ abstract class SWB {
return false;
}
if (firoTestnetWallets.isNotEmpty) {
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
for (final wallet in firoTestnetWallets) {
uiState?.update(
walletId: wallet.item2.walletId,
restoringStatus: StackRestoringStatus.restoring,
);
}
// try using node from backup first
NodeModel node = nodeService.getPrimaryNodeFor(coin: Coin.firoTestNet) ??
DefaultNodes.getNodeFor(Coin.firoTestNet);
final electrumxNode = ElectrumXNode(
address: node.host,
port: node.port,
name: node.name,
id: node.id,
useSSL: node.useSSL,
);
final failovers = nodeService.failoverNodesFor(coin: Coin.firoTestNet);
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
final cachedClient = CachedElectrumX.from(
node: electrumxNode,
prefs: _prefs,
failovers: failovers
.map(
(e) => ElectrumXNode(
address: e.host,
port: e.port,
name: e.name,
id: e.id,
useSSL: e.useSSL,
),
)
.toList(),
);
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
// Anonymity Set often fails when gathering from the server
const int maxTries = 5;
for (int j = 0; j < maxTries; j++) {
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
try {
await cachedClient.getAnonymitySet(
groupId: "1",
coin: Coin.firoTestNet,
);
break;
} catch (_) {
continue;
}
}
}
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
if (firoWallets.isNotEmpty) {
for (final wallet in firoWallets) {
uiState?.update(
walletId: wallet.item2.walletId,
restoringStatus: StackRestoringStatus.restoring,
);
}
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
// try using node from backup first
NodeModel node = nodeService.getPrimaryNodeFor(coin: Coin.firo) ??
DefaultNodes.getNodeFor(Coin.firo);
final electrumxNode = ElectrumXNode(
address: node.host,
port: node.port,
name: node.name,
id: node.id,
useSSL: node.useSSL,
);
final failovers = nodeService.failoverNodesFor(coin: Coin.firoTestNet);
final cachedClient = CachedElectrumX.from(
node: electrumxNode,
prefs: _prefs,
failovers: failovers
.map(
(e) => ElectrumXNode(
address: e.host,
port: e.port,
name: e.name,
id: e.id,
useSSL: e.useSSL,
),
)
.toList());
// Anonymity Set often fails when gathering from the server
const int maxTries = 5;
for (int j = 0; j < maxTries; j++) {
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
try {
await cachedClient.getAnonymitySet(
groupId: "1",
coin: Coin.firo,
);
break;
} catch (_) {
continue;
}
}
}
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
for (final tuple in firoTestnetWallets) {
restoreStatuses.add(asyncRestore(tuple, uiState, walletsService));
}
for (final tuple in firoWallets) {
restoreStatuses.add(asyncRestore(tuple, uiState, walletsService));
}
for (Future<bool> status in restoreStatuses) {
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
@ -869,13 +709,7 @@ abstract class SWB {
}
await status;
}
for (int i = 0; i < epicCashWallets.length; i++) {
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {
return false;
}
await asyncRestore(epicCashWallets[i], uiState, walletsService);
}
if (!Platform.isLinux) await Wakelock.disable();
// check if cancel was requested and restore previous state
if (_checkShouldCancel(preRestoreState)) {

View file

@ -8,6 +8,7 @@ import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
@ -15,7 +16,11 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:tuple/tuple.dart';
@ -40,6 +45,17 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
bool hidePassword = true;
Future<void> restoreBackupPopup(BuildContext context) async {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const RestoreBackupDialog();
},
);
}
@override
void initState() {
stackFileSystem = StackFileSystem();
@ -63,273 +79,322 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Restore from file",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
onTap: () async {
try {
await stackFileSystem.prepareStorage();
if (mounted) {
await stackFileSystem.openFile(context);
}
final isDesktop = Util.isDesktop;
if (mounted) {
return ConditionalParent(
condition: !isDesktop,
builder: (child) {
return Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Restore from file",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: child,
),
),
);
},
),
),
);
},
child: ConditionalParent(
condition: isDesktop,
builder: (child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
"Choose file location",
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
),
),
// child,
const SizedBox(height: 20),
Row(
children: [
PrimaryButton(
desktopMed: true,
width: 200,
label: "Restore",
onPressed: () {
restoreBackupPopup(context);
},
),
const SizedBox(width: 16),
SecondaryButton(
desktopMed: true,
width: 200,
label: "Cancel",
onPressed: () {},
),
],
),
],
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
onTap: () async {
try {
await stackFileSystem.prepareStorage();
if (mounted) {
await stackFileSystem.openFile(context);
}
if (mounted) {
setState(() {
fileLocationController.text =
stackFileSystem.filePath ?? "";
});
}
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
}
},
controller: fileLocationController,
style: STextStyles.field(context),
decoration: InputDecoration(
hintText: "Choose file...",
hintStyle: STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
SvgPicture.asset(
Assets.svg.folder,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
const SizedBox(
width: 12,
),
],
),
),
),
key: const Key("restoreFromFileLocationTextFieldKey"),
readOnly: true,
toolbarOptions: const ToolbarOptions(
copy: true,
cut: false,
paste: false,
selectAll: false,
),
onChanged: (newValue) {},
),
const SizedBox(
height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("restoreFromFilePasswordFieldKey"),
focusNode: passwordFocusNode,
controller: passwordController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Enter password",
passwordFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"restoreFromFilePasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
fileLocationController.text =
stackFileSystem.filePath ?? "";
hidePassword = !hidePassword;
});
}
} catch (e, s) {
Logging.instance
.log("$e\n$s", level: LogLevel.Error);
}
},
controller: fileLocationController,
style: STextStyles.field(context),
decoration: InputDecoration(
hintText: "Choose file...",
hintStyle: STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
onChanged: (newValue) {
setState(() {});
},
),
),
const SizedBox(
height: 16,
),
const Spacer(),
TextButton(
style: passwordController.text.isEmpty ||
fileLocationController.text.isEmpty
? Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonColor(context)
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: passwordController.text.isEmpty ||
fileLocationController.text.isEmpty
? null
: () async {
final String fileToRestore =
fileLocationController.text;
final String passphrase = passwordController.text;
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
if (!(await File(fileToRestore).exists())) {
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Backup file does not exist",
context: context,
);
return;
}
bool shouldPop = false;
showDialog<dynamic>(
barrierDismissible: false,
context: context,
builder: (_) => WillPopScope(
onWillPop: () async {
return shouldPop;
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
width: 16,
),
SvgPicture.asset(
Assets.svg.folder,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
Material(
color: Colors.transparent,
child: Center(
child: Text(
"Decrypting Stack backup file",
style: STextStyles.pageTitleH2(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textWhite,
),
),
),
),
const SizedBox(
width: 12,
height: 64,
),
const Center(
child: LoadingIndicator(
width: 100,
),
),
],
),
),
),
key: const Key("restoreFromFileLocationTextFieldKey"),
readOnly: true,
toolbarOptions: const ToolbarOptions(
copy: true,
cut: false,
paste: false,
selectAll: false,
),
onChanged: (newValue) {},
),
const SizedBox(
height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("restoreFromFilePasswordFieldKey"),
focusNode: passwordFocusNode,
controller: passwordController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Enter password",
passwordFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"restoreFromFilePasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
);
final String? jsonString = await compute(
SWB.decryptStackWalletWithPassphrase,
Tuple2(fileToRestore, passphrase),
debugLabel: "stack wallet decryption compute",
);
if (mounted) {
// pop LoadingIndicator
shouldPop = true;
Navigator.of(context).pop();
passwordController.text = "";
if (jsonString == null) {
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Failed to decrypt backup file",
context: context,
);
return;
}
Navigator.of(context).push(
RouteGenerator.getRoute(
builder: (_) => StackRestoreProgressView(
jsonString: jsonString,
),
),
),
onChanged: (newValue) {
setState(() {});
},
),
),
const SizedBox(
height: 16,
),
const Spacer(),
TextButton(
style: passwordController.text.isEmpty ||
fileLocationController.text.isEmpty
? Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonColor(context)
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: passwordController.text.isEmpty ||
fileLocationController.text.isEmpty
? null
: () async {
final String fileToRestore =
fileLocationController.text;
final String passphrase =
passwordController.text;
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
if (!(await File(fileToRestore).exists())) {
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Backup file does not exist",
context: context,
);
return;
}
bool shouldPop = false;
showDialog<dynamic>(
barrierDismissible: false,
context: context,
builder: (_) => WillPopScope(
onWillPop: () async {
return shouldPop;
},
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Material(
color: Colors.transparent,
child: Center(
child: Text(
"Decrypting Stack backup file",
style: STextStyles.pageTitleH2(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textWhite,
),
),
),
),
const SizedBox(
height: 64,
),
const Center(
child: LoadingIndicator(
width: 100,
),
),
],
),
),
);
final String? jsonString = await compute(
SWB.decryptStackWalletWithPassphrase,
Tuple2(fileToRestore, passphrase),
debugLabel: "stack wallet decryption compute",
);
if (mounted) {
// pop LoadingIndicator
shouldPop = true;
Navigator.of(context).pop();
passwordController.text = "";
if (jsonString == null) {
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Failed to decrypt backup file",
context: context,
);
return;
}
Navigator.of(context).push(
RouteGenerator.getRoute(
builder: (_) => StackRestoreProgressView(
jsonString: jsonString,
),
),
);
}
},
child: Text(
"Restore",
style: STextStyles.button(context),
),
),
],
),
);
}
},
child: Text(
"Restore",
style: STextStyles.button(context),
),
),
);
},
),
),
);
],
),
));
}
}

View file

@ -445,7 +445,7 @@ class _StackRestoreProgressViewState
},
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(context),
.getPrimaryEnabledButtonColor(context),
child: Text(
_success ? "OK" : "Cancel restore process",
style: STextStyles.button(context).copyWith(

View file

@ -6,7 +6,11 @@ import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/sync_type_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class SyncingOptionsView extends ConsumerWidget {
@ -16,384 +20,390 @@ class SyncingOptionsView extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
Navigator.of(context).pop();
},
),
title: Text(
"Syncing",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Column(
children: [
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
final isDesktop = Util.isDesktop;
return ConditionalParent(
condition: !isDesktop,
builder: (child) {
return Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
Navigator.of(context).pop();
},
),
title: Text(
"Syncing",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: child,
),
),
);
},
),
),
);
},
child: Column(
children: [
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.all(4),
child: RawMaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () {
final state =
ref.read(prefsChangeNotifierProvider).syncType;
if (state != SyncingType.currentWalletOnly) {
ref.read(prefsChangeNotifierProvider).syncType =
SyncingType.currentWalletOnly;
// disable auto sync on all wallets that aren't active/current
ref
.read(walletsChangeNotifierProvider)
.managers
.forEach((e) {
if (!e.isActiveWallet) {
e.shouldAutoSync = false;
}
});
}
},
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(4),
child: RawMaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
SizedBox(
width: 20,
height: 20,
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: SyncingType.currentWalletOnly,
groupValue: ref.watch(
prefsChangeNotifierProvider
.select((value) => value.syncType),
),
onPressed: () {
final state = ref
.read(prefsChangeNotifierProvider)
.syncType;
if (state != SyncingType.currentWalletOnly) {
onChanged: (value) {
if (value is SyncingType) {
ref
.read(prefsChangeNotifierProvider)
.syncType =
SyncingType.currentWalletOnly;
// disable auto sync on all wallets that aren't active/current
ref
.read(walletsChangeNotifierProvider)
.managers
.forEach((e) {
if (!e.isActiveWallet) {
e.shouldAutoSync = false;
}
});
}
},
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 20,
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value:
SyncingType.currentWalletOnly,
groupValue: ref.watch(
prefsChangeNotifierProvider
.select((value) =>
value.syncType),
),
onChanged: (value) {
if (value is SyncingType) {
ref
.read(
prefsChangeNotifierProvider)
.syncType = value;
}
},
),
),
const SizedBox(
width: 12,
),
Flexible(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"Sync only currently open wallet",
style: STextStyles.titleBold12(
context),
textAlign: TextAlign.left,
),
Text(
"Sync only the wallet that you are using",
style: STextStyles.itemSubtitle(
context),
textAlign: TextAlign.left,
),
],
),
),
],
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(4.0),
child: RawMaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () {
final state = ref
.read(prefsChangeNotifierProvider)
.syncType;
if (state !=
SyncingType.allWalletsOnStartup) {
ref
.read(prefsChangeNotifierProvider)
.syncType =
SyncingType.allWalletsOnStartup;
// enable auto sync on all wallets
ref
.read(walletsChangeNotifierProvider)
.managers
.forEach(
(e) => e.shouldAutoSync = true);
}
},
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 20,
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value:
SyncingType.allWalletsOnStartup,
groupValue: ref.watch(
prefsChangeNotifierProvider
.select((value) =>
value.syncType),
),
onChanged: (value) {
if (value is SyncingType) {
ref
.read(
prefsChangeNotifierProvider)
.syncType = value;
}
},
),
),
const SizedBox(
width: 12,
),
Flexible(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"Sync all wallets at startup",
style: STextStyles.titleBold12(
context),
textAlign: TextAlign.left,
),
Text(
"All of your wallets will start syncing when you open the app",
style: STextStyles.itemSubtitle(
context),
textAlign: TextAlign.left,
),
],
),
),
],
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(4),
child: RawMaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () {
final state = ref
.read(prefsChangeNotifierProvider)
.syncType;
if (state !=
SyncingType.selectedWalletsAtStartup) {
ref
.read(prefsChangeNotifierProvider)
.syncType =
SyncingType.selectedWalletsAtStartup;
final ids = ref
.read(prefsChangeNotifierProvider)
.walletIdsSyncOnStartup;
// enable auto sync on selected wallets only
ref
.read(walletsChangeNotifierProvider)
.managers
.forEach((e) => e.shouldAutoSync =
ids.contains(e.walletId));
.syncType = value;
}
},
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 20,
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: SyncingType
.selectedWalletsAtStartup,
groupValue: ref.watch(
prefsChangeNotifierProvider
.select((value) =>
value.syncType),
),
onChanged: (value) {
if (value is SyncingType) {
ref
.read(
prefsChangeNotifierProvider)
.syncType = value;
}
},
),
),
const SizedBox(
width: 12,
),
Flexible(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"Sync only selected wallets at startup",
style: STextStyles.titleBold12(
context),
textAlign: TextAlign.left,
),
Text(
"Only the wallets you select will start syncing when you open the app",
style: STextStyles.itemSubtitle(
context),
textAlign: TextAlign.left,
),
],
),
),
],
),
),
),
),
),
if (ref.watch(prefsChangeNotifierProvider
.select((value) => value.syncType)) !=
SyncingType.selectedWalletsAtStartup)
const SizedBox(
height: 12,
),
if (ref.watch(prefsChangeNotifierProvider
.select((value) => value.syncType)) ==
SyncingType.selectedWalletsAtStartup)
Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.only(
left: 12.0,
right: 12,
bottom: 12,
const SizedBox(
width: 12,
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Sync only currently open wallet",
style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const SizedBox(
width: 12 + 20,
height: 12,
),
Flexible(
child: RawMaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants
.size.circularBorderRadius,
),
),
onPressed: () {
Navigator.of(context).pushNamed(
WalletSyncingOptionsView
.routeName);
},
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"Select wallets...",
style:
STextStyles.link2(context),
textAlign: TextAlign.left,
),
],
),
),
),
],
Text(
"Sync only the wallet that you are using",
style: STextStyles.itemSubtitle(context),
textAlign: TextAlign.left,
),
),
],
),
),
],
),
),
],
),
),
),
),
);
},
),
Padding(
padding: const EdgeInsets.all(4.0),
child: RawMaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () {
final state =
ref.read(prefsChangeNotifierProvider).syncType;
if (state != SyncingType.allWalletsOnStartup) {
ref.read(prefsChangeNotifierProvider).syncType =
SyncingType.allWalletsOnStartup;
// enable auto sync on all wallets
ref
.read(walletsChangeNotifierProvider)
.managers
.forEach((e) => e.shouldAutoSync = true);
}
},
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 20,
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: SyncingType.allWalletsOnStartup,
groupValue: ref.watch(
prefsChangeNotifierProvider
.select((value) => value.syncType),
),
onChanged: (value) {
if (value is SyncingType) {
ref
.read(prefsChangeNotifierProvider)
.syncType = value;
}
},
),
),
const SizedBox(
width: 12,
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Sync all wallets at startup",
style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
Text(
"All of your wallets will start syncing when you open the app",
style: STextStyles.itemSubtitle(context),
textAlign: TextAlign.left,
),
],
),
),
],
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(4),
child: RawMaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () {
final state =
ref.read(prefsChangeNotifierProvider).syncType;
if (state != SyncingType.selectedWalletsAtStartup) {
ref.read(prefsChangeNotifierProvider).syncType =
SyncingType.selectedWalletsAtStartup;
final ids = ref
.read(prefsChangeNotifierProvider)
.walletIdsSyncOnStartup;
// enable auto sync on selected wallets only
ref
.read(walletsChangeNotifierProvider)
.managers
.forEach((e) =>
e.shouldAutoSync = ids.contains(e.walletId));
}
},
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 20,
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: SyncingType.selectedWalletsAtStartup,
groupValue: ref.watch(
prefsChangeNotifierProvider
.select((value) => value.syncType),
),
onChanged: (value) {
if (value is SyncingType) {
ref
.read(prefsChangeNotifierProvider)
.syncType = value;
}
},
),
),
const SizedBox(
width: 12,
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Sync only selected wallets at startup",
style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
Text(
"Only the wallets you select will start syncing when you open the app",
style: STextStyles.itemSubtitle(context),
textAlign: TextAlign.left,
),
],
),
),
],
),
),
),
),
),
if (ref.watch(prefsChangeNotifierProvider
.select((value) => value.syncType)) !=
SyncingType.selectedWalletsAtStartup)
const SizedBox(
height: 12,
),
if (ref.watch(prefsChangeNotifierProvider
.select((value) => value.syncType)) ==
SyncingType.selectedWalletsAtStartup)
Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.only(
left: 12.0,
right: 12,
bottom: 12,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
width: 12 + 20,
height: 12,
),
Flexible(
child: RawMaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () {
!isDesktop
? Navigator.of(context).pushNamed(
WalletSyncingOptionsView.routeName)
: showDialog(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return DesktopDialog(
maxWidth: 600,
maxHeight: 800,
child: Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Padding(
padding:
const EdgeInsets.all(
32),
child: Text(
"Select wallets to sync",
style: STextStyles
.desktopH3(context),
textAlign:
TextAlign.center,
),
),
const DesktopDialogCloseButton(),
],
),
const Expanded(
child:
WalletSyncingOptionsView(),
),
],
),
);
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Select wallets...",
style: STextStyles.link2(context),
textAlign: TextAlign.left,
),
],
),
),
),
],
),
),
),
],
),
),
],
),
);
}

View file

@ -10,7 +10,9 @@ import 'package:stackwallet/utilities/enums/sync_type_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/animated_text.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
@ -25,30 +27,47 @@ class WalletSyncingOptionsView extends ConsumerWidget {
final managers = ref
.watch(walletsChangeNotifierProvider.select((value) => value.managers));
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
Navigator.of(context).pop();
},
),
title: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
"Sync only selected wallets at startup",
style: STextStyles.navBarTitle(context),
final isDesktop = Util.isDesktop;
return ConditionalParent(
condition: !isDesktop,
builder: (child) {
return Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
Navigator.of(context).pop();
},
),
title: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
"Sync only selected wallets at startup",
style: STextStyles.navBarTitle(context),
),
),
),
),
),
body: LayoutBuilder(builder: (context, constraints) {
return Padding(
padding: const EdgeInsets.only(
left: 12,
top: 12,
right: 12,
body: Padding(
padding: const EdgeInsets.only(
left: 12,
top: 12,
right: 12,
),
child: child,
),
child: SingleChildScrollView(
);
},
child: ConditionalParent(
condition: isDesktop,
builder: (child) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 32),
child: child,
);
},
child: LayoutBuilder(builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 24,
@ -71,6 +90,11 @@ class WalletSyncingOptionsView extends ConsumerWidget {
),
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
borderColor: !isDesktop
? Colors.transparent
: Theme.of(context)
.extension<StackColors>()!
.background,
child: Column(
children: [
...managers.map(
@ -208,9 +232,9 @@ class WalletSyncingOptionsView extends ConsumerWidget {
),
),
),
),
);
}),
);
}),
),
);
}
}

View file

@ -1,6 +1,11 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class ConfirmFullRescanDialog extends StatelessWidget {
@ -11,40 +16,110 @@ class ConfirmFullRescanDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
return true;
},
child: StackDialog(
title: "Rescan blockchain",
message:
"Warning! It may take a while. If you exit before completion, you will have to redo the process.",
leftButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(context),
child: Text(
"Cancel",
style: STextStyles.itemSubtitle12(context),
),
onPressed: () {
Navigator.of(context).pop();
},
if (Util.isDesktop) {
return DesktopDialog(
maxWidth: 576,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(
left: 32,
),
child: Text(
"Rescan blockchain",
style: STextStyles.desktopH3(context),
),
),
const DesktopDialogCloseButton(),
],
),
Padding(
padding: const EdgeInsets.only(
top: 8,
left: 32,
right: 32,
bottom: 32,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Warning! It may take a while. If you exit before completion, you will have to redo the process.",
style: STextStyles.desktopTextSmall(context),
),
const SizedBox(
height: 43,
),
Row(
children: [
Expanded(
child: SecondaryButton(
desktopMed: true,
onPressed: Navigator.of(context).pop,
label: "Cancel",
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
desktopMed: true,
onPressed: () {
Navigator.of(context).pop();
onConfirm.call();
},
label: "Rescan",
),
),
],
)
],
),
)
],
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
child: Text(
"Rescan",
style: STextStyles.button(context),
);
} else {
return WillPopScope(
onWillPop: () async {
return true;
},
child: StackDialog(
title: "Rescan blockchain",
message:
"Warning! It may take a while. If you exit before completion, you will have to redo the process.",
leftButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(context),
child: Text(
"Cancel",
style: STextStyles.itemSubtitle12(context),
),
onPressed: () {
Navigator.of(context).pop();
},
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
child: Text(
"Rescan",
style: STextStyles.button(context),
),
onPressed: () {
Navigator.of(context).pop();
onConfirm.call();
},
),
onPressed: () {
Navigator.of(context).pop();
onConfirm.call();
},
),
),
);
);
}
}
}

View file

@ -30,6 +30,8 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:tuple/tuple.dart';
import 'package:stackwallet/utilities/util.dart';
/// [eventBus] should only be set during testing
class WalletSettingsView extends StatefulWidget {
const WalletSettingsView({
@ -374,6 +376,8 @@ class _EpiBoxInfoFormState extends ConsumerState<EpicBoxInfoForm> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: hostController,
decoration: const InputDecoration(hintText: "Host"),
),
@ -381,6 +385,8 @@ class _EpiBoxInfoFormState extends ConsumerState<EpicBoxInfoForm> {
height: 8,
),
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: portController,
decoration: const InputDecoration(hintText: "Port"),
keyboardType: const TextInputType.numberWithOptions(),

View file

@ -11,6 +11,8 @@ import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:stackwallet/utilities/util.dart';
class RenameWalletView extends ConsumerStatefulWidget {
const RenameWalletView({
Key? key,
@ -74,6 +76,8 @@ class _RenameWalletViewState extends ConsumerState<RenameWalletView> {
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _controller,
focusNode: _focusNode,
style: STextStyles.field(context),

View file

@ -1,15 +1,24 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart';
import 'package:stackwallet/pages_desktop_specific/create_password/create_password_view.dart';
import 'package:stackwallet/providers/global/prefs_provider.dart';
import 'package:stackwallet/providers/global/price_provider.dart';
import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class StackPrivacyCalls extends ConsumerStatefulWidget {
@ -46,140 +55,212 @@ class _StackPrivacyCalls extends ConsumerState<StackPrivacyCalls> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
),
return MasterScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
isDesktop: isDesktop,
appBar: isDesktop
? const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
)
: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(0, 40, 0, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Choose your Stack experience",
style: STextStyles.pageTitleH1(context),
),
const SizedBox(
height: 8,
),
Text(
"You can change it later in Settings",
style: STextStyles.subtitle(context),
),
const SizedBox(
height: 36,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
child: ConditionalParent(
condition: !isDesktop,
builder: (child) => LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: PrivacyToggle(
externalCallsEnabled: isEasy,
onChanged: (externalCalls) {
isEasy = externalCalls;
setState(() {
infoToggle = isEasy;
});
},
child: IntrinsicHeight(
child: child,
),
),
const SizedBox(
height: 36,
),
),
child: Padding(
padding: EdgeInsets.fromLTRB(0, isDesktop ? 0 : 40, 0, 0),
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: isDesktop ? 480 : double.infinity,
),
Padding(
padding: const EdgeInsets.all(16.0),
child: RoundedWhiteContainer(
child: Center(
child: RichText(
textAlign: TextAlign.left,
text: TextSpan(
style:
STextStyles.label(context).copyWith(fontSize: 12.0),
children: infoToggle
? [
const TextSpan(
text:
"Exchange data preloaded for a seamless experience."),
const TextSpan(
text:
"\n\nCoinGecko enabled: (24 hour price change shown in-app, total wallet value shown in USD or other currency)."),
TextSpan(
text:
"\n\nRecommended for most crypto users.",
style: TextStyle(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
fontWeight: FontWeight.w600,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Choose your Stack experience",
style: isDesktop
? STextStyles.desktopH2(context)
: STextStyles.pageTitleH1(context),
),
SizedBox(
height: isDesktop ? 16 : 8,
),
Text(
!widget.isSettings
? "You can change it later in Settings"
: "",
style: isDesktop
? STextStyles.desktopSubtitleH2(context)
: STextStyles.subtitle(context),
),
SizedBox(
height: isDesktop ? 32 : 36,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? 0 : 16,
),
child: PrivacyToggle(
externalCallsEnabled: isEasy,
onChanged: (externalCalls) {
isEasy = externalCalls;
setState(() {
infoToggle = isEasy;
});
},
),
),
SizedBox(
height: isDesktop ? 16 : 36,
),
Padding(
padding: isDesktop
? const EdgeInsets.all(0)
: const EdgeInsets.all(16.0),
child: RoundedWhiteContainer(
child: Center(
child: RichText(
textAlign: TextAlign.left,
text: TextSpan(
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.label(context).copyWith(
fontSize: 12.0,
),
),
]
: [
const TextSpan(
text:
"Exchange data not preloaded (slower experience)."),
const TextSpan(
text:
"\n\nCoinGecko disabled (price changes not shown, no wallet value shown in other currencies)."),
TextSpan(
text:
"\n\nRecommended for the privacy conscious.",
style: TextStyle(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
fontWeight: FontWeight.w600,
),
),
],
children: infoToggle
? [
const TextSpan(
text:
"Exchange data preloaded for a seamless experience."),
const TextSpan(
text:
"\n\nCoinGecko enabled: (24 hour price change shown in-app, total wallet value shown in USD or other currency)."),
TextSpan(
text:
"\n\nRecommended for most crypto users.",
style: isDesktop
? STextStyles
.desktopTextExtraExtraSmall600(
context)
: TextStyle(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
fontWeight: FontWeight.w600,
),
),
]
: [
const TextSpan(
text:
"Exchange data not preloaded (slower experience)."),
const TextSpan(
text:
"\n\nCoinGecko disabled (price changes not shown, no wallet value shown in other currencies)."),
TextSpan(
text:
"\n\nRecommended for the privacy conscious.",
style: isDesktop
? STextStyles
.desktopTextExtraExtraSmall600(
context)
: TextStyle(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
),
),
),
const Spacer(
flex: 4,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Row(
children: [
Expanded(
child: ContinueButton(
isDesktop: isDesktop,
label: !widget.isSettings ? "Continue" : "Save changes",
onPressed: () {
ref.read(prefsChangeNotifierProvider).externalCalls =
isEasy;
if (!widget.isSettings) {
if (isDesktop) {
Navigator.of(context).pushNamed(
CreatePasswordView.routeName,
);
} else {
Navigator.of(context).pushNamed(
CreatePinView.routeName,
);
}
} else {
Navigator.pop(context);
}
},
),
if (!isDesktop)
const Spacer(
flex: 4,
),
],
),
if (isDesktop)
const SizedBox(
height: 32,
),
Padding(
padding: isDesktop
? const EdgeInsets.all(0)
: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Row(
children: [
Expanded(
child: PrimaryButton(
label:
!widget.isSettings ? "Continue" : "Save changes",
onPressed: () {
ref
.read(prefsChangeNotifierProvider)
.externalCalls = isEasy;
DB.instance
.put<dynamic>(
boxName: DB.boxNamePrefs,
key: "externalCalls",
value: isEasy)
.then((_) {
if (isEasy) {
unawaited(
ExchangeDataLoadingService().loadAll(ref));
ref
.read(priceAnd24hChangeNotifierProvider)
.start(true);
}
});
if (!widget.isSettings) {
if (isDesktop) {
Navigator.of(context).pushNamed(
CreatePasswordView.routeName,
);
} else {
Navigator.of(context).pushNamed(
CreatePinView.routeName,
);
}
} else {
Navigator.pop(context);
}
},
),
),
],
),
),
if (isDesktop)
const SizedBox(
height: kDesktopAppBarHeight,
),
],
),
],
),
),
),
),
@ -204,8 +285,11 @@ class PrivacyToggle extends StatefulWidget {
class _PrivacyToggleState extends State<PrivacyToggle> {
late bool externalCallsEnabled;
late final bool isDesktop;
@override
void initState() {
isDesktop = Util.isDesktop;
// initial toggle state
externalCallsEnabled = widget.externalCallsEnabled;
super.initState();
@ -217,6 +301,7 @@ class _PrivacyToggleState extends State<PrivacyToggle> {
children: [
Expanded(
child: RawMaterialButton(
elevation: 0,
fillColor: Theme.of(context).extension<StackColors>()!.popupBG,
shape: RoundedRectangleBorder(
side: !externalCallsEnabled
@ -248,24 +333,39 @@ class _PrivacyToggleState extends State<PrivacyToggle> {
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (isDesktop)
const SizedBox(
height: 10,
),
SvgPicture.asset(
Assets.svg.personaEasy,
width: 140,
height: 140,
width: isDesktop ? 120 : 140,
height: isDesktop ? 120 : 140,
),
Center(
child: Text(
"Easy Crypto",
style: STextStyles.label(context).copyWith(
fontWeight: FontWeight.bold,
if (isDesktop)
const SizedBox(
height: 12,
),
)),
Center(
child: Text(
"Easy Crypto",
style: isDesktop
? STextStyles.desktopTextSmall(context)
: STextStyles.label700(context),
),
),
Center(
child: Text(
"Recommended",
style: STextStyles.label(context),
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.label(context),
),
),
if (isDesktop)
const SizedBox(
height: 12,
),
],
),
if (externalCallsEnabled)
@ -338,25 +438,39 @@ class _PrivacyToggleState extends State<PrivacyToggle> {
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (isDesktop)
const SizedBox(
height: 10,
),
SvgPicture.asset(
Assets.svg.personaIncognito,
width: 140,
height: 140,
width: isDesktop ? 120 : 140,
height: isDesktop ? 120 : 140,
),
if (isDesktop)
const SizedBox(
height: 12,
),
Center(
child: Text(
"Incognito",
style: STextStyles.label(context).copyWith(
fontWeight: FontWeight.bold,
),
style: isDesktop
? STextStyles.desktopTextSmall(context)
: STextStyles.label700(context),
),
),
Center(
child: Text(
"Privacy conscious",
style: STextStyles.label(context),
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.label(context),
),
),
if (isDesktop)
const SizedBox(
height: 12,
),
],
),
if (!externalCallsEnabled)
@ -396,158 +510,3 @@ class _PrivacyToggleState extends State<PrivacyToggle> {
);
}
}
class ContinueButton extends ConsumerWidget {
const ContinueButton({
Key? key,
required this.isDesktop,
required this.onPressed,
required this.label,
}) : super(key: key);
final String label;
final bool isDesktop;
final VoidCallback onPressed;
@override
Widget build(BuildContext context, WidgetRef ref) {
if (isDesktop) {
return SizedBox(
width: 328,
height: 70,
child: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: onPressed,
child: Text(
label,
style: STextStyles.button(context).copyWith(fontSize: 20),
),
),
);
} else {
return TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: onPressed,
child: Text(
label,
style: STextStyles.button(context),
),
);
}
}
}
// class CustomRadio extends StatefulWidget {
// CustomRadio(this.upperCall, {Key? key}) : super(key: key);
//
// Function upperCall;
//
// @override
// createState() {
// return CustomRadioState();
// }
// }
//
// class CustomRadioState extends State<CustomRadio> {
// List<RadioModel> sampleData = <RadioModel>[];
//
// @override
// void initState() {
// super.initState();
// sampleData.add(
// RadioModel(true, Assets.svg.personaEasy, 'Easy Crypto', 'Recommended'));
// sampleData.add(RadioModel(
// false, Assets.svg.personaIncognito, 'Incognito', 'Privacy conscious'));
// }
//
// @override
// Widget build(BuildContext context) {
// return Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// InkWell(
// onTap: () {
// setState(() {
// // if (!sampleData[0].isSelected) {
// widget.upperCall.call(true);
// // }
// for (var element in sampleData) {
// element.isSelected = false;
// }
// sampleData[0].isSelected = true;
// });
// },
// child: RadioItem(sampleData[0]),
// ),
// InkWell(
// onTap: () {
// setState(() {
// // if (!sampleData[1].isSelected) {
// widget.upperCall.call(false);
// // }
// for (var element in sampleData) {
// element.isSelected = false;
// }
// sampleData[1].isSelected = true;
// });
// },
// child: RadioItem(sampleData[1]),
// )
// ],
// );
// }
// }
//
// class RadioItem extends StatelessWidget {
// final RadioModel _item;
// const RadioItem(this._item, {Key? key}) : super(key: key);
// @override
// Widget build(BuildContext context) {
// return Container(
// margin: const EdgeInsets.all(15.0),
// child: RoundedWhiteContainer(
// borderColor: _item.isSelected ? const Color(0xFF0056D2) : null,
// child: Center(
// child: Column(
// children: [
// SvgPicture.asset(
// _item.svg,
// // color: Theme.of(context).extension<StackColors>()!.textWhite,
// width: 140,
// height: 140,
// ),
// RichText(
// textAlign: TextAlign.center,
// text: TextSpan(
// style: STextStyles.label(context).copyWith(fontSize: 12.0),
// children: [
// TextSpan(
// text: _item.topText,
// style: TextStyle(
// color: Theme.of(context)
// .extension<StackColors>()!
// .textDark,
// fontWeight: FontWeight.bold)),
// TextSpan(text: "\n${_item.bottomText}"),
// ],
// ),
// ),
// ],
// )),
// ),
// );
// }
// }
//
// class RadioModel {
// bool isSelected;
// final String svg;
// final String topText;
// final String bottomText;
//
// RadioModel(this.isSelected, this.svg, this.topText, this.bottomText);
// }

View file

@ -10,6 +10,7 @@ import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/trade_card.dart';
import 'package:stackwallet/widgets/transaction_card.dart';
@ -67,6 +68,65 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
);
}
Widget itemBuilder(
BuildContext context, Transaction tx, BorderRadius? radius) {
final matchingTrades = ref
.read(tradesServiceProvider)
.trades
.where((e) => e.payInTxid == tx.txid || e.payOutTxid == tx.txid);
if (tx.txType == "Sent" && matchingTrades.isNotEmpty) {
final trade = matchingTrades.first;
return Container(
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: radius,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TransactionCard(
// this may mess with combined firo transactions
key: Key(tx.toString()), //
transaction: tx,
walletId: widget.walletId,
),
TradeCard(
// this may mess with combined firo transactions
key: Key(tx.toString() + trade.uuid), //
trade: trade,
onTap: () {
unawaited(
Navigator.of(context).pushNamed(
TradeDetailsView.routeName,
arguments: Tuple4(
trade.tradeId,
tx,
widget.walletId,
ref.read(managerProvider).walletName,
),
),
);
},
)
],
),
);
} else {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: radius,
),
child: TransactionCard(
// this may mess with combined firo transactions
key: Key(tx.toString()), //
transaction: tx,
walletId: widget.walletId,
),
);
}
}
@override
void initState() {
managerProvider = widget.managerProvider;
@ -119,77 +179,42 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
unawaited(ref.read(managerProvider).refresh());
}
},
child: ListView.builder(
itemCount: list.length,
itemBuilder: (context, index) {
BorderRadius? radius;
if (index == list.length - 1) {
radius = _borderRadiusLast;
} else if (index == 0) {
radius = _borderRadiusFirst;
}
final tx = list[index];
final matchingTrades = ref
.read(tradesServiceProvider)
.trades
.where((e) =>
e.payInTxid == tx.txid || e.payOutTxid == tx.txid);
if (tx.txType == "Sent" && matchingTrades.isNotEmpty) {
final trade = matchingTrades.first;
return Container(
decoration: BoxDecoration(
color:
Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: radius,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TransactionCard(
// this may mess with combined firo transactions
key: Key(tx.toString()), //
transaction: tx,
walletId: widget.walletId,
),
TradeCard(
// this may mess with combined firo transactions
key: Key(tx.toString() + trade.uuid), //
trade: trade,
onTap: () {
unawaited(
Navigator.of(context).pushNamed(
TradeDetailsView.routeName,
arguments: Tuple4(
trade.tradeId,
tx,
widget.walletId,
ref.read(managerProvider).walletName,
),
),
);
},
)
],
),
);
} else {
return Container(
decoration: BoxDecoration(
color:
Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: radius,
),
child: TransactionCard(
// this may mess with combined firo transactions
key: Key(tx.toString()), //
transaction: tx,
walletId: widget.walletId,
),
);
}
},
),
child: Util.isDesktop
? ListView.separated(
itemBuilder: (context, index) {
BorderRadius? radius;
if (index == list.length - 1) {
radius = _borderRadiusLast;
} else if (index == 0) {
radius = _borderRadiusFirst;
}
final tx = list[index];
return itemBuilder(context, tx, radius);
},
separatorBuilder: (context, index) {
return Container(
width: double.infinity,
height: 2,
color: Theme.of(context)
.extension<StackColors>()!
.background,
);
},
itemCount: list.length,
)
: ListView.builder(
itemCount: list.length,
itemBuilder: (context, index) {
BorderRadius? radius;
if (index == list.length - 1) {
radius = _borderRadiusLast;
} else if (index == 0) {
radius = _borderRadiusFirst;
}
final tx = list[index];
return itemBuilder(context, tx, radius);
},
),
);
}
},

View file

@ -10,6 +10,7 @@ import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
/// [eventBus] should only be set during testing
class WalletRefreshButton extends ConsumerStatefulWidget {
@ -70,7 +71,7 @@ class _RefreshButtonState extends ConsumerState<WalletRefreshButton>
_spinController?.stop();
break;
case WalletSyncStatus.syncing:
_spinController?.repeat();
unawaited(_spinController?.repeat());
break;
}
}
@ -92,10 +93,15 @@ class _RefreshButtonState extends ConsumerState<WalletRefreshButton>
@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
return SizedBox(
height: 36,
width: 36,
height: isDesktop ? 22 : 36,
width: isDesktop ? 22 : 36,
child: MaterialButton(
color: isDesktop
? Theme.of(context).extension<StackColors>()!.buttonBackSecondary
: null,
splashColor: Theme.of(context).extension<StackColors>()!.highlight,
onPressed: () {
final managerProvider = ref
@ -110,6 +116,9 @@ class _RefreshButtonState extends ConsumerState<WalletRefreshButton>
.then((_) => _spinController?.stop());
}
},
elevation: 0,
highlightElevation: 0,
hoverElevation: 0,
padding: EdgeInsets.zero,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
@ -121,9 +130,13 @@ class _RefreshButtonState extends ConsumerState<WalletRefreshButton>
turns: _spinAnimation,
child: SvgPicture.asset(
Assets.svg.arrowRotate,
width: 24,
height: 24,
color: Theme.of(context).extension<StackColors>()!.textFavoriteCard,
width: isDesktop ? 12 : 24,
height: isDesktop ? 12 : 24,
color: isDesktop
? Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultSearchIconRight
: Theme.of(context).extension<StackColors>()!.textFavoriteCard,
),
),
),

View file

@ -1,18 +1,32 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/contact.dart';
import 'package:stackwallet/models/paymint/transactions_model.dart';
import 'package:stackwallet/models/transaction_filter.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_search_filter_view.dart';
import 'package:stackwallet/providers/global/address_book_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
@ -84,8 +98,12 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> {
}
final date = DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000);
if (date.millisecondsSinceEpoch > filter.to.millisecondsSinceEpoch ||
date.millisecondsSinceEpoch < filter.from.millisecondsSinceEpoch) {
if ((filter.to != null &&
date.millisecondsSinceEpoch >
filter.to!.millisecondsSinceEpoch) ||
(filter.from != null &&
date.millisecondsSinceEpoch <
filter.from!.millisecondsSinceEpoch)) {
return false;
}
@ -113,13 +131,25 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> {
.isNotEmpty;
// check if address contains
contains |= tx.address.contains(keyword);
contains |= tx.address.toLowerCase().contains(keyword);
// check if note contains
contains |= notes[tx.txid] != null && notes[tx.txid]!.contains(keyword);
contains |= notes[tx.txid] != null &&
notes[tx.txid]!.toLowerCase().contains(keyword);
// check if txid contains
contains |= tx.txid.contains(keyword);
contains |= tx.txid.toLowerCase().contains(keyword);
// check if subType contains
contains |=
tx.subType.isNotEmpty && tx.subType.toLowerCase().contains(keyword);
// check if txType contains
contains |= tx.txType.toLowerCase().contains(keyword);
// check if date contains
contains |=
Format.extractDateFrom(tx.timestamp).toLowerCase().contains(keyword);
return contains;
}
@ -164,123 +194,249 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Transactions",
style: STextStyles.navBarTitle(context),
),
actions: [
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 20,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("transactionSearchFilterViewButton"),
size: 36,
shadows: const [],
color: Theme.of(context).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.filter,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
width: 20,
height: 20,
),
onPressed: () {
Navigator.of(context).pushNamed(
TransactionSearchFilterView.routeName,
arguments: ref
.read(walletsChangeNotifierProvider)
.getManager(walletId)
.coin,
);
final isDesktop = Util.isDesktop;
return MasterScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
isDesktop: isDesktop,
appBar: isDesktop
? DesktopAppBar(
isCompactHeight: true,
background: Theme.of(context).extension<StackColors>()!.popupBG,
leading: Row(
children: [
const SizedBox(
width: 32,
),
AppBarIconButton(
size: 32,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
shadows: const [],
icon: SvgPicture.asset(
Assets.svg.arrowLeft,
width: 18,
height: 18,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: Navigator.of(context).pop,
),
const SizedBox(
width: 12,
),
Text(
"Transactions",
style: STextStyles.desktopH3(context),
),
],
),
)
: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Transactions",
style: STextStyles.navBarTitle(context),
),
actions: [
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 20,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("transactionSearchFilterViewButton"),
size: 36,
shadows: const [],
color: Theme.of(context)
.extension<StackColors>()!
.background,
icon: SvgPicture.asset(
Assets.svg.filter,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
width: 20,
height: 20,
),
onPressed: () {
Navigator.of(context).pushNamed(
TransactionSearchFilterView.routeName,
arguments: ref
.read(walletsChangeNotifierProvider)
.getManager(walletId)
.coin,
);
},
),
),
),
],
),
),
],
),
body: Padding(
padding: const EdgeInsets.only(
left: 12,
top: 12,
right: 12,
padding: EdgeInsets.only(
left: isDesktop ? 20 : 12,
top: isDesktop ? 20 : 12,
right: isDesktop ? 20 : 12,
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(4),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
controller: _searchController,
focusNode: searchFieldFocusNode,
onChanged: (value) {
setState(() {
_searchString = value;
});
},
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Search",
searchFieldFocusNode,
context,
).copyWith(
prefixIcon: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 16,
),
child: SvgPicture.asset(
Assets.svg.search,
width: 16,
height: 16,
),
child: Row(
children: [
ConditionalParent(
condition: isDesktop,
builder: (child) => SizedBox(
width: 570,
child: child,
),
suffixIcon: _searchController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_searchController.text = "";
});
},
),
],
child: ConditionalParent(
condition: !isDesktop,
builder: (child) => Expanded(
child: child,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: !isDesktop,
enableSuggestions: !isDesktop,
controller: _searchController,
focusNode: searchFieldFocusNode,
onChanged: (value) {
setState(() {
_searchString = value;
});
},
style: isDesktop
? STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveText,
height: 1.8,
)
: STextStyles.field(context),
decoration: standardInputDecoration(
"Search...",
searchFieldFocusNode,
context,
desktopMed: isDesktop,
).copyWith(
prefixIcon: Padding(
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? 12 : 10,
vertical: isDesktop ? 18 : 16,
),
child: SvgPicture.asset(
Assets.svg.search,
width: isDesktop ? 20 : 16,
height: isDesktop ? 20 : 16,
),
),
)
: null,
suffixIcon: _searchController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_searchController.text = "";
_searchString = "";
});
},
),
],
),
),
)
: null,
),
),
),
),
),
),
if (isDesktop)
const SizedBox(
width: 20,
),
if (isDesktop)
SecondaryButton(
desktopMed: isDesktop,
width: 200,
label: "Filter",
icon: SvgPicture.asset(
Assets.svg.filter,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
width: 20,
height: 20,
),
onPressed: () {
final coin = ref
.read(walletsChangeNotifierProvider)
.getManager(walletId)
.coin;
if (isDesktop) {
showDialog<void>(
context: context,
builder: (context) {
return TransactionSearchFilterView(
coin: coin,
);
},
);
} else {
Navigator.of(context).pushNamed(
TransactionSearchFilterView.routeName,
arguments: coin,
);
}
},
),
],
),
),
if (isDesktop)
const SizedBox(
height: 8,
),
if (isDesktop &&
ref.watch(transactionFilterProvider.state).state != null)
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
),
child: Row(
children: const [
TransactionFilterOptionBar(),
],
),
),
const SizedBox(
height: 8,
),
@ -313,6 +469,7 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> {
final monthlyList = groupTransactionsByMonth(searched);
return ListView.builder(
primary: isDesktop ? false : null,
itemCount: monthlyList.length,
itemBuilder: (_, index) {
final month = monthlyList[index];
@ -332,21 +489,48 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> {
const SizedBox(
height: 12,
),
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: Column(
children: [
...month.item2.map(
(tx) => TransactionCard(
if (isDesktop)
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: ListView.separated(
shrinkWrap: true,
primary: false,
separatorBuilder: (context, _) =>
Container(
height: 1,
color: Theme.of(context)
.extension<StackColors>()!
.background,
),
itemCount: month.item2.length,
itemBuilder: (context, index) =>
Padding(
padding: const EdgeInsets.all(4),
child: DesktopTransactionCardRow(
key: Key(
"transactionCard_key_${tx.txid}"),
transaction: tx,
"transactionCard_key_${month.item2[index].txid}"),
transaction: month.item2[index],
walletId: walletId,
),
),
],
),
),
if (!isDesktop)
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: Column(
children: [
...month.item2.map(
(tx) => TransactionCard(
key: Key(
"transactionCard_key_${tx.txid}"),
transaction: tx,
walletId: walletId,
),
),
],
),
),
),
],
),
);
@ -367,3 +551,445 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> {
);
}
}
class TransactionFilterOptionBar extends ConsumerStatefulWidget {
const TransactionFilterOptionBar({Key? key}) : super(key: key);
@override
ConsumerState<TransactionFilterOptionBar> createState() =>
_TransactionFilterOptionBarState();
}
class _TransactionFilterOptionBarState
extends ConsumerState<TransactionFilterOptionBar> {
final List<TransactionFilterOptionBarItem> items = [];
TransactionFilter? _filter;
@override
void initState() {
_filter = ref.read(transactionFilterProvider.state).state;
if (_filter != null) {
if (_filter!.sent) {
const label = "Sent";
final item = TransactionFilterOptionBarItem(
label: label,
onPressed: (s) {
items.removeWhere((e) => e.label == label);
if (items.isEmpty) {
ref.read(transactionFilterProvider.state).state = null;
} else {
ref.read(transactionFilterProvider.state).state =
ref.read(transactionFilterProvider.state).state?.copyWith(
sent: false,
);
setState(() {});
}
},
);
items.add(item);
}
if (_filter!.received) {
const label = ("Received");
final item = TransactionFilterOptionBarItem(
label: label,
onPressed: (s) {
items.removeWhere((e) => e.label == label);
if (items.isEmpty) {
ref.read(transactionFilterProvider.state).state = null;
} else {
ref.read(transactionFilterProvider.state).state =
ref.read(transactionFilterProvider.state).state?.copyWith(
received: false,
);
setState(() {});
}
},
);
items.add(item);
}
if (_filter!.to != null) {
final label = _filter!.from.toString();
final item = TransactionFilterOptionBarItem(
label: label,
onPressed: (s) {
items.removeWhere((e) => e.label == label);
if (items.isEmpty) {
ref.read(transactionFilterProvider.state).state = null;
} else {
ref.read(transactionFilterProvider.state).state =
ref.read(transactionFilterProvider.state).state?.copyWith(
to: null,
);
setState(() {});
}
},
);
items.add(item);
}
if (_filter!.from != null) {
final label2 = _filter!.to.toString();
final item2 = TransactionFilterOptionBarItem(
label: label2,
onPressed: (s) {
items.removeWhere((e) => e.label == label2);
if (items.isEmpty) {
ref.read(transactionFilterProvider.state).state = null;
} else {
ref.read(transactionFilterProvider.state).state =
ref.read(transactionFilterProvider.state).state?.copyWith(
from: null,
);
setState(() {});
}
},
);
items.add(item2);
}
if (_filter!.amount != null) {
final label = _filter!.amount!.toString();
final item = TransactionFilterOptionBarItem(
label: label,
onPressed: (s) {
items.removeWhere((e) => e.label == label);
if (items.isEmpty) {
ref.read(transactionFilterProvider.state).state = null;
} else {
ref.read(transactionFilterProvider.state).state =
ref.read(transactionFilterProvider.state).state?.copyWith(
amount: null,
);
setState(() {});
}
},
);
items.add(item);
}
if (_filter!.keyword.isNotEmpty) {
final label = _filter!.keyword;
final item = TransactionFilterOptionBarItem(
label: label,
onPressed: (s) {
items.removeWhere((e) => e.label == label);
if (items.isEmpty) {
ref.read(transactionFilterProvider.state).state = null;
} else {
ref.read(transactionFilterProvider.state).state =
ref.read(transactionFilterProvider.state).state?.copyWith(
keyword: "",
);
setState(() {});
}
},
);
items.add(item);
}
}
super.initState();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 32,
child: ListView.separated(
primary: false,
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: items.length,
separatorBuilder: (_, __) => const SizedBox(
width: 16,
),
itemBuilder: (context, index) => items[index],
),
);
}
}
class TransactionFilterOptionBarItem extends StatelessWidget {
const TransactionFilterOptionBarItem({
Key? key,
required this.label,
this.onPressed,
}) : super(key: key);
final String label;
final void Function(String)? onPressed;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onPressed?.call(label),
child: Container(
height: 32,
decoration: BoxDecoration(
color:
Theme.of(context).extension<StackColors>()!.buttonBackSecondary,
borderRadius: BorderRadius.circular(1000)),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 14,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
label,
textAlign: TextAlign.center,
style: STextStyles.labelExtraExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark,
),
),
),
),
const SizedBox(
width: 10,
),
XIcon(
width: 16,
height: 16,
color: Theme.of(context).extension<StackColors>()!.textDark,
),
],
),
),
),
);
}
}
class DesktopTransactionCardRow extends ConsumerStatefulWidget {
const DesktopTransactionCardRow({
Key? key,
required this.transaction,
required this.walletId,
}) : super(key: key);
final Transaction transaction;
final String walletId;
@override
ConsumerState<DesktopTransactionCardRow> createState() =>
_DesktopTransactionCardRowState();
}
class _DesktopTransactionCardRowState
extends ConsumerState<DesktopTransactionCardRow> {
late final Transaction _transaction;
late final String walletId;
String whatIsIt(String type, Coin coin) {
if (coin == Coin.epicCash && _transaction.slateId == null) {
return "Restored Funds";
}
if (_transaction.subType == "mint") {
if (_transaction.confirmedStatus) {
return "Anonymized";
} else {
return "Anonymizing";
}
}
if (type == "Received") {
if (_transaction.confirmedStatus) {
return "Received";
} else {
return "Receiving";
}
} else if (type == "Sent") {
if (_transaction.confirmedStatus) {
return "Sent";
} else {
return "Sending";
}
} else {
return type;
}
}
@override
void initState() {
walletId = widget.walletId;
_transaction = widget.transaction;
super.initState();
}
@override
Widget build(BuildContext context) {
final locale = ref.watch(
localeServiceChangeNotifierProvider.select((value) => value.locale));
final manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId)));
final baseCurrency = ref
.watch(prefsChangeNotifierProvider.select((value) => value.currency));
final coin = manager.coin;
final price = ref
.watch(priceAnd24hChangeNotifierProvider
.select((value) => value.getPrice(coin)))
.item1;
late final String prefix;
if (Util.isDesktop) {
if (_transaction.txType == "Sent") {
prefix = "-";
} else if (_transaction.txType == "Received") {
prefix = "+";
}
} else {
prefix = "";
}
return Material(
color: Theme.of(context).extension<StackColors>()!.popupBG,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(Constants.size.circularBorderRadius),
),
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () async {
if (coin == Coin.epicCash && _transaction.slateId == null) {
unawaited(
showFloatingFlushBar(
context: context,
message:
"Restored Epic funds from your Seed have no Data.\nUse Stack Backup to keep your transaction history.",
type: FlushBarType.warning,
duration: const Duration(seconds: 5),
),
);
return;
}
if (Util.isDesktop) {
await showDialog<void>(
context: context,
builder: (context) => DesktopDialog(
maxHeight: MediaQuery.of(context).size.height - 64,
maxWidth: 580,
child: TransactionDetailsView(
transaction: _transaction,
coin: coin,
walletId: walletId,
),
),
);
} else {
unawaited(
Navigator.of(context).pushNamed(
TransactionDetailsView.routeName,
arguments: Tuple3(
_transaction,
coin,
walletId,
),
),
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
child: Row(
children: [
TxIcon(transaction: _transaction),
const SizedBox(
width: 12,
),
Expanded(
flex: 3,
child: Text(
_transaction.isCancelled
? "Cancelled"
: whatIsIt(_transaction.txType, coin),
style:
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
),
),
Expanded(
flex: 4,
child: Text(
Format.extractDateFrom(_transaction.timestamp),
style: STextStyles.label(context),
),
),
Expanded(
flex: 6,
child: Builder(
builder: (_) {
final amount = coin == Coin.monero
? (_transaction.amount ~/ 10000)
: coin == Coin.wownero
? (_transaction.amount ~/ 1000)
: _transaction.amount;
return Text(
"$prefix${Format.satoshiAmountToPrettyString(amount, locale)} ${coin.ticker}",
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
);
},
),
),
if (ref.watch(prefsChangeNotifierProvider
.select((value) => value.externalCalls)))
Expanded(
flex: 4,
child: Builder(
builder: (_) {
// TODO: modify Format.<functions> to take optional Coin parameter so this type oif check isn't done in ui
int value = _transaction.amount;
if (coin == Coin.monero) {
value = (value ~/ 10000);
} else if (coin == Coin.wownero) {
value = (value ~/ 1000);
}
return Text(
"$prefix${Format.localizedStringAsFixed(
value: Format.satoshisToAmount(value) * price,
locale: locale,
decimalPlaces: 2,
)} $baseCurrency",
style: STextStyles.desktopTextExtraExtraSmall(context),
);
},
),
),
SvgPicture.asset(
Assets.svg.circleInfo,
width: 20,
height: 20,
color:
Theme.of(context).extension<StackColors>()!.textSubtitle2,
),
],
),
),
),
);
}
}

View file

@ -4,7 +4,10 @@ import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
@ -31,8 +34,11 @@ class _EditNoteViewState extends ConsumerState<EditNoteView> {
late final TextEditingController _noteController;
final noteFieldFocusNode = FocusNode();
late final bool isDesktop;
@override
void initState() {
isDesktop = Util.isDesktop;
_noteController = TextEditingController();
_noteController.text = widget.note;
super.initState();
@ -48,29 +54,178 @@ class _EditNoteViewState extends ConsumerState<EditNoteView> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Edit note",
style: STextStyles.navBarTitle(context),
),
backgroundColor: isDesktop
? Colors.transparent
: Theme.of(context).extension<StackColors>()!.background,
appBar: isDesktop
? null
: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Edit note",
style: STextStyles.navBarTitle(context),
),
),
body: MobileEditNoteScaffold(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (isDesktop)
Padding(
padding: const EdgeInsets.only(
left: 32,
bottom: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Edit note",
style: STextStyles.desktopH3(context),
),
const DesktopDialogCloseButton(),
],
),
),
Padding(
padding: isDesktop
? const EdgeInsets.symmetric(
horizontal: 32,
)
: const EdgeInsets.all(0),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _noteController,
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveText,
height: 1.8,
)
: STextStyles.field(context),
focusNode: noteFieldFocusNode,
decoration: standardInputDecoration(
"Note",
noteFieldFocusNode,
context,
desktopMed: isDesktop,
).copyWith(
contentPadding: isDesktop
? const EdgeInsets.only(
left: 16,
top: 11,
bottom: 12,
right: 5,
)
: null,
suffixIcon: _noteController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_noteController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
),
// if (!isDesktop)
const Spacer(),
if (isDesktop)
Padding(
padding: const EdgeInsets.all(32),
child: PrimaryButton(
label: "Save",
onPressed: () async {
await ref
.read(
notesServiceChangeNotifierProvider(widget.walletId))
.editOrAddNote(
txid: widget.txid,
note: _noteController.text,
);
if (mounted) {
Navigator.of(context).pop();
}
},
),
),
if (!isDesktop)
TextButton(
onPressed: () async {
await ref
.read(notesServiceChangeNotifierProvider(widget.walletId))
.editOrAddNote(
txid: widget.txid,
note: _noteController.text,
);
if (mounted) {
Navigator.of(context).pop();
}
},
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
child: Text(
"Save",
style: STextStyles.button(context),
),
)
],
),
body: Padding(
padding: const EdgeInsets.all(12),
child: LayoutBuilder(builder: (context, constraints) {
),
);
}
}
class MobileEditNoteScaffold extends StatelessWidget {
const MobileEditNoteScaffold({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
if (Util.isDesktop) {
return child;
} else {
return Padding(
padding: const EdgeInsets.all(12),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
@ -79,73 +234,14 @@ class _EditNoteViewState extends ConsumerState<EditNoteView> {
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
controller: _noteController,
style: STextStyles.field(context),
focusNode: noteFieldFocusNode,
decoration: standardInputDecoration(
"Note",
noteFieldFocusNode,
context,
).copyWith(
suffixIcon: _noteController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_noteController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
const Spacer(),
TextButton(
onPressed: () async {
await ref
.read(notesServiceChangeNotifierProvider(
widget.walletId))
.editOrAddNote(
txid: widget.txid,
note: _noteController.text,
);
if (mounted) {
Navigator.of(context).pop();
}
},
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
child: Text(
"Save",
style: STextStyles.button(context),
),
)
],
),
child: child,
),
),
),
);
}),
));
},
),
);
}
}
}

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/utilities/assets.dart';
@ -11,6 +12,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:tuple/tuple.dart';
class FavoriteCard extends ConsumerStatefulWidget {
@ -54,13 +56,20 @@ class _FavoriteCardState extends ConsumerState<FavoriteCard> {
return GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(
WalletView.routeName,
arguments: Tuple2(
walletId,
managerProvider,
),
);
if (Util.isDesktop) {
Navigator.of(context).pushNamed(
DesktopWalletView.routeName,
arguments: walletId,
);
} else {
Navigator.of(context).pushNamed(
WalletView.routeName,
arguments: Tuple2(
walletId,
managerProvider,
),
);
}
},
child: SizedBox(
width: widget.width,

View file

@ -1,10 +1,12 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
@ -18,7 +20,7 @@ import 'package:stackwallet/widgets/progress_bar.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:zxcvbn/zxcvbn.dart';
class CreatePasswordView extends StatefulWidget {
class CreatePasswordView extends ConsumerStatefulWidget {
const CreatePasswordView({
Key? key,
this.secureStore = const SecureStorageWrapper(
@ -31,10 +33,10 @@ class CreatePasswordView extends StatefulWidget {
final FlutterSecureStorageInterface secureStore;
@override
State<CreatePasswordView> createState() => _CreatePasswordViewState();
ConsumerState<CreatePasswordView> createState() => _CreatePasswordViewState();
}
class _CreatePasswordViewState extends State<CreatePasswordView> {
class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> {
late final TextEditingController passwordController;
late final TextEditingController passwordRepeatController;
@ -76,8 +78,16 @@ class _CreatePasswordViewState extends State<CreatePasswordView> {
return;
}
await widget.secureStore
.write(key: "stackDesktopPassword", value: passphrase);
try {
await ref.read(storageCryptoHandlerProvider).initFromNew(passphrase);
} catch (e) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "Error: $e",
context: context,
));
return;
}
if (mounted) {
unawaited(Navigator.of(context)

View file

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
class DesktopLoginView extends StatefulWidget {
const DesktopLoginView({
Key? key,
this.startupWalletId,
}) : super(key: key);
static const String routeName = "/desktopLogin";
final String? startupWalletId;
@override
State<DesktopLoginView> createState() => _DesktopLoginViewState();
}
class _DesktopLoginViewState extends State<DesktopLoginView> {
@override
Widget build(BuildContext context) {
return Material(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"Login",
style: STextStyles.desktopH3(context),
),
PrimaryButton(
label: "Login",
onPressed: () {
// todo auth
Navigator.of(context).pushNamedAndRemoveUntil(
DesktopHomeView.routeName,
(route) => false,
);
},
)
],
),
);
}
}

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart';
import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
@ -19,6 +19,7 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> {
int currentViewIndex = 0;
final List<Widget> contentViews = [
const Navigator(
key: Key("desktopStackHomeKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: MyStackView.routeName,
),
@ -32,8 +33,9 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> {
color: Colors.orange,
),
const Navigator(
key: Key("desktopSettingHomeKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: SettingsMenu.routeName,
initialRoute: DesktopSettingsView.routeName,
),
Container(
color: Colors.blue,
@ -41,9 +43,6 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> {
Container(
color: Colors.pink,
),
Container(
color: Colors.purple,
),
];
void onMenuSelectionChanged(int newIndex) {
@ -61,6 +60,10 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> {
DesktopMenu(
onSelectionChanged: onMenuSelectionChanged,
),
Container(
width: 1,
color: Theme.of(context).extension<StackColors>()!.background,
),
Expanded(
child: contentViews[currentViewIndex],
),

View file

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
@ -70,136 +72,197 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
const SizedBox(
height: 60,
),
SizedBox(
width: _width == expandedWidth
? _width - 32 // 16 padding on either side
: _width - 16, // 8 padding on either side
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.walletFa,
width: 20,
height: 20,
Expanded(
child: SizedBox(
width: _width == expandedWidth
? _width - 32 // 16 padding on either side
: _width - 16, // 8 padding on either side
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.walletDesktop,
width: 20,
height: 20,
color: 0 == selectedMenuItem
? Theme.of(context)
.extension<StackColors>()!
.textDark
: Theme.of(context)
.extension<StackColors>()!
.textDark
.withOpacity(0.8),
),
label: "My Stack",
value: 0,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
label: "My Stack",
value: 0,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
const SizedBox(
height: 2,
),
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.exchange3,
width: 20,
height: 20,
const SizedBox(
height: 2,
),
label: "Exchange",
value: 1,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
const SizedBox(
height: 2,
),
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.bell,
width: 20,
height: 20,
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.exchangeDesktop,
width: 20,
height: 20,
color: 1 == selectedMenuItem
? Theme.of(context)
.extension<StackColors>()!
.textDark
: Theme.of(context)
.extension<StackColors>()!
.textDark
.withOpacity(0.8),
),
label: "Exchange",
value: 1,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
label: "Notifications",
value: 2,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
const SizedBox(
height: 2,
),
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.addressBook2,
width: 20,
height: 20,
const SizedBox(
height: 2,
),
label: "Address Book",
value: 3,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
const SizedBox(
height: 2,
),
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.gear,
width: 20,
height: 20,
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.bell,
width: 20,
height: 20,
color: 2 == selectedMenuItem
? Theme.of(context)
.extension<StackColors>()!
.textDark
: Theme.of(context)
.extension<StackColors>()!
.textDark
.withOpacity(0.8),
),
label: "Notifications",
value: 2,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
label: "Settings",
value: 4,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
const SizedBox(
height: 2,
),
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.messageQuestion,
width: 20,
height: 20,
const SizedBox(
height: 2,
),
label: "Support",
value: 5,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
const SizedBox(
height: 2,
),
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.messageQuestion,
width: 20,
height: 20,
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.addressBookDesktop,
width: 20,
height: 20,
color: 3 == selectedMenuItem
? Theme.of(context)
.extension<StackColors>()!
.textDark
: Theme.of(context)
.extension<StackColors>()!
.textDark
.withOpacity(0.8),
),
label: "Address Book",
value: 3,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
label: "About",
value: 6,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
const SizedBox(
height: 2,
),
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.messageQuestion,
width: 20,
height: 20,
const SizedBox(
height: 2,
),
label: "Exit",
value: 7,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
],
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.gear,
width: 20,
height: 20,
color: 4 == selectedMenuItem
? Theme.of(context)
.extension<StackColors>()!
.textDark
: Theme.of(context)
.extension<StackColors>()!
.textDark
.withOpacity(0.8),
),
label: "Settings",
value: 4,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
const SizedBox(
height: 2,
),
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.messageQuestion,
width: 20,
height: 20,
color: 5 == selectedMenuItem
? Theme.of(context)
.extension<StackColors>()!
.textDark
: Theme.of(context)
.extension<StackColors>()!
.textDark
.withOpacity(0.8),
),
label: "Support",
value: 5,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
const SizedBox(
height: 2,
),
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.aboutDesktop,
width: 20,
height: 20,
color: 6 == selectedMenuItem
? Theme.of(context)
.extension<StackColors>()!
.textDark
: Theme.of(context)
.extension<StackColors>()!
.textDark
.withOpacity(0.8),
),
label: "About",
value: 6,
group: selectedMenuItem,
onChanged: updateSelectedMenuItem,
iconOnly: _width == minimizedWidth,
),
const Spacer(),
DesktopMenuItem(
icon: SvgPicture.asset(
Assets.svg.exitDesktop,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.textDark
.withOpacity(0.8),
),
label: "Exit",
value: 7,
group: selectedMenuItem,
onChanged: (_) {
// todo: save stuff/ notify before exit?
exit(0);
},
iconOnly: _width == minimizedWidth,
),
],
),
),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Spacer(),
IconButton(
@ -212,7 +275,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
),
),
],
)
),
],
),
),

View file

@ -1,7 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_settings.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart';
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
class DesktopSettingsView extends ConsumerStatefulWidget {
const DesktopSettingsView({Key? key}) : super(key: key);
@ -16,29 +28,45 @@ class DesktopSettingsView extends ConsumerStatefulWidget {
class _DesktopSettingsViewState extends ConsumerState<DesktopSettingsView> {
int currentViewIndex = 0;
final List<Widget> contentViews = [
Container(
color: Colors.lime,
const Navigator(
key: Key("settingsBackupRestoreDesktopKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: BackupRestoreSettings.routeName,
), //b+r
Container(
color: Colors.green,
const Navigator(
key: Key("settingsSecurityDesktopKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: SecuritySettings.routeName,
), //security
Container(
color: Colors.red,
const Navigator(
key: Key("settingsCurrencyDesktopKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: CurrencySettings.routeName,
), //currency
Container(
color: Colors.orange,
const Navigator(
key: Key("settingsLanguageDesktopKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: LanguageOptionSettings.routeName,
), //language
Container(
color: Colors.yellow,
const Navigator(
key: Key("settingsNodesDesktopKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: NodesSettings.routeName,
), //nodes
Container(
color: Colors.blue,
const Navigator(
key: Key("settingsSyncingPreferencesDesktopKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: SyncingPreferencesSettings.routeName,
), //syncing prefs
Container(
color: Colors.pink,
const Navigator(
key: Key("settingsAppearanceDesktopKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: AppearanceOptionSettings.routeName,
), //appearance
Container(
color: Colors.purple,
const Navigator(
key: Key("settingsAdvancedDesktopKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: AdvancedSettings.routeName,
), //advanced
];
@ -48,12 +76,26 @@ class _DesktopSettingsViewState extends ConsumerState<DesktopSettingsView> {
});
}
// will have a row with two items: SettingsMenu and settings contentxd
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).extension<StackColors>()!.background,
child: Row(
return DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: true,
leading: Row(
children: [
const SizedBox(
width: 24,
height: 24,
),
Text(
"Settings",
style: STextStyles.desktopH3(context),
)
],
),
),
body: Row(
children: [
SettingsMenu(
onSelectionChanged: onMenuSelectionChanged,

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart';
@ -37,6 +38,12 @@ class CoinWalletsTable extends ConsumerWidget {
),
WalletInfoRow(
walletId: walletIds[i],
onPressed: () async {
await Navigator.of(context).pushNamed(
DesktopWalletView.routeName,
arguments: walletIds[i],
);
},
),
],
),

View file

@ -0,0 +1,396 @@
import 'dart:async';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart';
import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/my_wallet.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart';
import 'package:stackwallet/providers/global/auto_swb_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/hover_text_field.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:tuple/tuple.dart';
/// [eventBus] should only be set during testing
class DesktopWalletView extends ConsumerStatefulWidget {
const DesktopWalletView({
Key? key,
required this.walletId,
this.eventBus,
}) : super(key: key);
static const String routeName = "/desktopWalletView";
final String walletId;
final EventBus? eventBus;
@override
ConsumerState<DesktopWalletView> createState() => _DesktopWalletViewState();
}
class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
late final TextEditingController controller;
late final String walletId;
late final EventBus eventBus;
late final bool _shouldDisableAutoSyncOnLogOut;
final _cnLoadingService = ExchangeDataLoadingService();
Future<void> onBackPressed() async {
await _logout();
if (mounted) {
Navigator.of(context).pop();
}
}
Future<void> _logout() async {
final managerProvider =
ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId);
if (_shouldDisableAutoSyncOnLogOut) {
// disable auto sync if it was enabled only when loading wallet
ref.read(managerProvider).shouldAutoSync = false;
}
ref.read(managerProvider.notifier).isActiveWallet = false;
ref.read(transactionFilterProvider.state).state = null;
if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled &&
ref.read(prefsChangeNotifierProvider).backupFrequencyType ==
BackupFrequencyType.afterClosingAWallet) {
unawaited(ref.read(autoSWBServiceProvider).doBackup());
}
}
void _loadCNData() {
// unawaited future
if (ref.read(prefsChangeNotifierProvider).externalCalls) {
_cnLoadingService.loadAll(ref,
coin: ref
.read(walletsChangeNotifierProvider)
.getManager(walletId)
.coin);
} else {
Logging.instance.log("User does not want to use external calls",
level: LogLevel.Info);
}
}
void _onExchangePressed(BuildContext context) async {
final managerProvider =
ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId);
unawaited(_cnLoadingService.loadAll(ref));
final coin = ref.read(managerProvider).coin;
if (coin == Coin.epicCash) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Exchange not available for Epic Cash",
),
);
} else if (coin.name.endsWith("TestNet")) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Exchange not available for test net coins",
),
);
} else {
ref.read(currentExchangeNameStateProvider.state).state =
ChangeNowExchange.exchangeName;
final walletId = ref.read(managerProvider).walletId;
ref.read(prefsChangeNotifierProvider).exchangeRateType =
ExchangeRateType.estimated;
ref.read(exchangeFormStateProvider).exchange = ref.read(exchangeProvider);
ref.read(exchangeFormStateProvider).exchangeType =
ExchangeRateType.estimated;
final currencies = ref
.read(availableChangeNowCurrenciesProvider)
.currencies
.where((element) =>
element.ticker.toLowerCase() == coin.ticker.toLowerCase());
if (currencies.isNotEmpty) {
ref.read(exchangeFormStateProvider).setCurrencies(
currencies.first,
ref
.read(availableChangeNowCurrenciesProvider)
.currencies
.firstWhere(
(element) =>
element.ticker.toLowerCase() !=
coin.ticker.toLowerCase(),
),
);
}
if (mounted) {
unawaited(
Navigator.of(context).pushNamed(
WalletInitiatedExchangeView.routeName,
arguments: Tuple3(
walletId,
coin,
_loadCNData,
),
),
);
}
}
}
@override
void initState() {
controller = TextEditingController();
walletId = widget.walletId;
final managerProvider =
ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId);
controller.text = ref.read(managerProvider).walletName;
eventBus =
widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance;
ref.read(managerProvider).isActiveWallet = true;
if (!ref.read(managerProvider).shouldAutoSync) {
// enable auto sync if it wasn't enabled when loading wallet
ref.read(managerProvider).shouldAutoSync = true;
_shouldDisableAutoSyncOnLogOut = true;
} else {
_shouldDisableAutoSyncOnLogOut = false;
}
ref.read(managerProvider).refresh();
super.initState();
}
@override
Widget build(BuildContext context) {
final manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId)));
final coin = manager.coin;
final managerProvider = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManagerProvider(walletId)));
return DesktopScaffold(
appBar: DesktopAppBar(
background: Theme.of(context).extension<StackColors>()!.popupBG,
leading: Expanded(
child: Row(
children: [
const SizedBox(
width: 32,
),
AppBarIconButton(
size: 32,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
shadows: const [],
icon: SvgPicture.asset(
Assets.svg.arrowLeft,
width: 18,
height: 18,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: onBackPressed,
),
const SizedBox(
width: 15,
),
SvgPicture.asset(
Assets.svg.iconFor(coin: coin),
width: 32,
height: 32,
),
const SizedBox(
width: 12,
),
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 48,
),
child: IntrinsicWidth(
child: HoverTextField(
controller: controller,
style: STextStyles.desktopH3(context),
readOnly: true,
onDone: () async {
final currentWalletName =
ref.read(managerProvider).walletName;
final newName = controller.text;
if (newName != currentWalletName) {
final success = await ref
.read(walletsServiceChangeNotifierProvider)
.renameWallet(
from: currentWalletName,
to: newName,
shouldNotifyListeners: true,
);
if (success) {
ref
.read(walletsChangeNotifierProvider)
.getManager(walletId)
.walletName = newName;
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "Wallet renamed",
context: context,
),
);
} else {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message:
"Wallet named \"$newName\" already exists",
context: context,
),
);
controller.text = currentWalletName;
}
}
},
),
),
),
const Spacer(),
Row(
children: [
NetworkInfoButton(
walletId: walletId,
eventBus: eventBus,
),
const SizedBox(
width: 32,
),
WalletKeysButton(
walletId: walletId,
),
const SizedBox(
width: 32,
),
],
),
],
),
),
useSpacers: false,
isCompactHeight: true,
),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
RoundedWhiteContainer(
padding: const EdgeInsets.all(20),
child: Row(
children: [
SvgPicture.asset(
Assets.svg.iconFor(coin: coin),
width: 40,
height: 40,
),
const SizedBox(
width: 10,
),
DesktopWalletSummary(
walletId: walletId,
managerProvider: managerProvider,
initialSyncStatus: ref.watch(managerProvider
.select((value) => value.isRefreshing))
? WalletSyncStatus.syncing
: WalletSyncStatus.synced,
),
const Spacer(),
SecondaryButton(
width: 180,
desktopMed: true,
onPressed: () {
_onExchangePressed(context);
},
label: "Exchange",
icon: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
color: Theme.of(context)
.extension<StackColors>()!
.buttonBackPrimary
.withOpacity(0.2),
),
child: Center(
child: SvgPicture.asset(
Assets.svg.arrowRotate2,
width: 14,
height: 14,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
),
),
),
],
),
),
const SizedBox(
height: 24,
),
Expanded(
child: Row(
children: [
SizedBox(
width: 450,
child: MyWallet(
walletId: walletId,
),
),
const SizedBox(
width: 16,
),
Expanded(
child: RecentDesktopTransactions(
walletId: walletId,
),
),
],
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,260 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/models/contact.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart';
import 'package:stackwallet/providers/global/address_book_service_provider.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class AddressBookAddressChooser extends StatefulWidget {
const AddressBookAddressChooser({
Key? key,
this.coin,
}) : super(key: key);
final Coin? coin;
@override
State<AddressBookAddressChooser> createState() =>
_AddressBookAddressChooserState();
}
class _AddressBookAddressChooserState extends State<AddressBookAddressChooser> {
late final bool isDesktop;
late final TextEditingController _searchController;
late final FocusNode searchFieldFocusNode;
String _searchTerm = "";
int _compareContactFavorite(Contact a, Contact b) {
if (a.isFavorite && b.isFavorite) {
return 0;
} else if (a.isFavorite) {
return 1;
} else {
return -1;
}
}
List<Contact> pullOutFavorites(List<Contact> contacts) {
final List<Contact> favorites = [];
contacts.removeWhere((contact) {
if (contact.isFavorite) {
favorites.add(contact);
return true;
}
return false;
});
return favorites;
}
List<Contact> filter(List<Contact> contacts, String searchTerm) {
if (widget.coin != null) {
contacts.removeWhere(
(e) => e.addresses.where((a) => a.coin == widget.coin!).isEmpty);
}
contacts.retainWhere((e) => _matches(searchTerm, e));
if (contacts.length < 2) {
return contacts;
}
// redundant due to pullOutFavorites?
contacts.sort(_compareContactFavorite);
return contacts;
}
bool _matches(String term, Contact contact) {
final text = term.toLowerCase();
if (contact.name.toLowerCase().contains(text)) {
return true;
}
for (int i = 0; i < contact.addresses.length; i++) {
if (contact.addresses[i].label.toLowerCase().contains(text) ||
contact.addresses[i].coin.name.toLowerCase().contains(text) ||
contact.addresses[i].coin.prettyName.toLowerCase().contains(text) ||
contact.addresses[i].coin.ticker.toLowerCase().contains(text) ||
contact.addresses[i].address.toLowerCase().contains(text)) {
return true;
}
}
return false;
}
@override
void initState() {
isDesktop = Util.isDesktop;
searchFieldFocusNode = FocusNode();
_searchController = TextEditingController();
super.initState();
}
@override
void dispose() {
_searchController.dispose();
searchFieldFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// search field
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: !isDesktop,
enableSuggestions: !isDesktop,
controller: _searchController,
focusNode: searchFieldFocusNode,
onChanged: (value) {
setState(() {
_searchTerm = value;
});
},
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveText,
height: 1.8,
)
: STextStyles.field(context),
decoration: standardInputDecoration(
"Search",
searchFieldFocusNode,
context,
desktopMed: isDesktop,
).copyWith(
prefixIcon: Padding(
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? 12 : 10,
vertical: isDesktop ? 18 : 16,
),
child: SvgPicture.asset(
Assets.svg.search,
width: isDesktop ? 20 : 16,
height: isDesktop ? 20 : 16,
),
),
suffixIcon: _searchController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_searchController.text = "";
_searchTerm = "";
});
},
),
],
),
),
)
: null,
),
),
),
),
const SizedBox(
height: 16,
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
bottom: 32,
),
child: Consumer(
builder: (context, ref, _) {
List<Contact> contacts = ref
.watch(addressBookServiceProvider
.select((value) => value.contacts))
.toList();
contacts = filter(contacts, _searchTerm);
final favorites = pullOutFavorites(contacts);
return ListView.builder(
primary: false,
shrinkWrap: true,
itemCount: favorites.length +
contacts.length +
2, // +2 for "fav" and "all" headers
itemBuilder: (context, index) {
if (index == 0) {
return Padding(
key: const Key(
"addressBookCAddressChooserFavoritesHeaderItemKey"),
padding: const EdgeInsets.only(
bottom: 10,
),
child: Text(
"Favorites",
style:
STextStyles.desktopTextExtraExtraSmall(context),
),
);
} else if (index <= favorites.length) {
final id = favorites[index - 1].id;
return ContactListItem(
key: Key("contactContactListItem_${id}_key"),
contactId: id,
filterByCoin: widget.coin,
);
} else if (index == favorites.length + 1) {
return Padding(
key: const Key(
"addressBookCAddressChooserAllContactsHeaderItemKey"),
padding: const EdgeInsets.symmetric(
vertical: 10,
),
child: Text(
"All contacts",
style:
STextStyles.desktopTextExtraExtraSmall(context),
),
);
} else {
final id = contacts[index - favorites.length - 1].id;
return ContactListItem(
key: Key("contactContactListItem_${id}_key"),
contactId: id,
filterByCoin: widget.coin,
);
}
},
);
},
),
),
),
],
);
}
}

View file

@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/providers/global/address_book_service_provider.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/address_book_card.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/expandable.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart';
class ContactListItem extends ConsumerStatefulWidget {
const ContactListItem({
Key? key,
required this.contactId,
this.filterByCoin,
}) : super(key: key);
final String contactId;
final Coin? filterByCoin;
@override
ConsumerState<ContactListItem> createState() => _ContactListItemState();
}
class _ContactListItemState extends ConsumerState<ContactListItem> {
late final String contactId;
late final Coin? filterByCoin;
ExpandableState _state = ExpandableState.collapsed;
@override
void initState() {
contactId = widget.contactId;
filterByCoin = widget.filterByCoin;
super.initState();
}
@override
Widget build(BuildContext context) {
final contact = ref.watch(addressBookServiceProvider
.select((value) => value.getContactById(contactId)));
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
borderColor: Theme.of(context).extension<StackColors>()!.background,
child: Expandable(
onExpandChanged: (state) {
setState(() {
_state = state;
});
},
header: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 14,
),
child: AddressBookCard(
contactId: contactId,
indicatorDown: _state == ExpandableState.expanded,
),
),
body: Column(
mainAxisSize: MainAxisSize.min,
children: [
// filter addresses by coin is provided before building address list
...contact.addresses
.where((e) =>
filterByCoin != null ? e.coin == filterByCoin! : true)
.map(
(e) => Column(
key: Key("contactAddress_${e.address}_${e.label}_key"),
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 1,
color: Theme.of(context)
.extension<StackColors>()!
.background,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 14,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
WalletInfoCoinIcon(coin: e.coin),
const SizedBox(
width: 12,
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${contactId == "default" ? e.other! : e.label} (${e.coin.ticker})",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
Text(
e.address,
style: STextStyles
.desktopTextExtraExtraSmall(context),
),
],
),
],
),
BlueTextButton(
text: "Select wallet",
onTap: () {
Navigator.of(context).pop(e);
},
),
],
),
)
],
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,312 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:tuple/tuple.dart';
class DesktopReceive extends ConsumerStatefulWidget {
const DesktopReceive({
Key? key,
required this.walletId,
this.clipboard = const ClipboardWrapper(),
}) : super(key: key);
final String walletId;
final ClipboardInterface clipboard;
@override
ConsumerState<DesktopReceive> createState() => _DesktopReceiveState();
}
class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
late final Coin coin;
late final String walletId;
late final ClipboardInterface clipboard;
Future<void> generateNewAddress() async {
bool shouldPop = false;
unawaited(
showDialog(
context: context,
builder: (_) {
return WillPopScope(
onWillPop: () async => shouldPop,
child: Container(
color: Theme.of(context)
.extension<StackColors>()!
.overlay
.withOpacity(0.5),
child: const CustomLoadingOverlay(
message: "Generating address",
eventBus: null,
),
),
);
},
),
);
await ref
.read(walletsChangeNotifierProvider)
.getManager(walletId)
.generateNewAddress();
shouldPop = true;
if (mounted) {
Navigator.of(context, rootNavigator: true).pop();
}
}
String receivingAddress = "";
@override
void initState() {
walletId = widget.walletId;
coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin;
clipboard = widget.clipboard;
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final address = await ref
.read(walletsChangeNotifierProvider)
.getManager(walletId)
.currentReceivingAddress;
setState(() {
receivingAddress = address;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
ref.listen(
ref
.read(walletsChangeNotifierProvider)
.getManagerProvider(walletId)
.select((value) => value.currentReceivingAddress),
(previous, next) {
if (next is Future<String>) {
next.then((value) => setState(() => receivingAddress = value));
}
});
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
GestureDetector(
onTap: () {
clipboard.setData(
ClipboardData(text: receivingAddress),
);
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
);
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).extension<StackColors>()!.background,
width: 2,
),
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
child: RoundedWhiteContainer(
child: Column(
children: [
Row(
children: [
Text(
"Your ${coin.ticker} address",
style: STextStyles.itemSubtitle(context),
),
const Spacer(),
Row(
children: [
SvgPicture.asset(
Assets.svg.copy,
width: 15,
height: 15,
color: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
),
const SizedBox(
width: 4,
),
Text(
"Copy",
style: STextStyles.link2(context),
),
],
),
],
),
const SizedBox(
height: 8,
),
Row(
children: [
Expanded(
child: Text(
receivingAddress,
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
),
],
),
],
),
),
),
),
if (coin != Coin.epicCash)
const SizedBox(
height: 20,
),
if (coin != Coin.epicCash)
SecondaryButton(
desktopMed: true,
onPressed: generateNewAddress,
label: "Generate new address",
),
const SizedBox(
height: 32,
),
Center(
child: QrImage(
data: "${coin.uriScheme}:$receivingAddress",
size: 200,
foregroundColor:
Theme.of(context).extension<StackColors>()!.accentColorDark,
),
),
const SizedBox(
height: 32,
),
// TODO: create transparent button class to account for hover
GestureDetector(
onTap: () async {
if (Util.isDesktop) {
await showDialog<void>(
context: context,
builder: (context) => DesktopDialog(
maxHeight: double.infinity,
maxWidth: 580,
child: Column(
children: [
Row(
children: [
const AppBarBackButton(
size: 40,
iconSize: 24,
),
Text(
"Generate QR code",
style: STextStyles.desktopH3(context),
),
],
),
IntrinsicHeight(
child: Navigator(
onGenerateRoute: RouteGenerator.generateRoute,
onGenerateInitialRoutes: (_, __) => [
RouteGenerator.generateRoute(
RouteSettings(
name: GenerateUriQrCodeView.routeName,
arguments: Tuple2(coin, receivingAddress),
),
),
],
),
),
],
),
),
);
} else {
unawaited(
Navigator.of(context).push(
RouteGenerator.getRoute(
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
builder: (_) => GenerateUriQrCodeView(
coin: coin,
receivingAddress: receivingAddress,
),
settings: const RouteSettings(
name: GenerateUriQrCodeView.routeName,
),
),
),
);
}
},
child: Container(
color: Colors.transparent,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SvgPicture.asset(
Assets.svg.qrcode,
width: 14,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorBlue,
),
const SizedBox(
width: 8,
),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
"Create new QR code",
style: STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorBlue,
),
),
),
],
),
),
),
],
);
}
}

View file

@ -0,0 +1,283 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/animated_text.dart';
class DesktopWalletSummary extends StatefulWidget {
const DesktopWalletSummary({
Key? key,
required this.walletId,
required this.managerProvider,
required this.initialSyncStatus,
}) : super(key: key);
final String walletId;
final ChangeNotifierProvider<Manager> managerProvider;
final WalletSyncStatus initialSyncStatus;
@override
State<DesktopWalletSummary> createState() => _WDesktopWalletSummaryState();
}
class _WDesktopWalletSummaryState extends State<DesktopWalletSummary> {
late final String walletId;
late final ChangeNotifierProvider<Manager> managerProvider;
void showSheet() {
showModalBottomSheet<dynamic>(
backgroundColor: Colors.transparent,
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
builder: (_) => WalletBalanceToggleSheet(walletId: walletId),
);
}
Decimal? _balanceTotalCached;
Decimal? _balanceCached;
@override
void initState() {
walletId = widget.walletId;
managerProvider = widget.managerProvider;
super.initState();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
Consumer(
builder: (_, ref, __) {
final Coin coin =
ref.watch(managerProvider.select((value) => value.coin));
final externalCalls = ref.watch(prefsChangeNotifierProvider
.select((value) => value.externalCalls));
Future<Decimal>? totalBalanceFuture;
Future<Decimal>? availableBalanceFuture;
if (coin == Coin.firo || coin == Coin.firoTestNet) {
final firoWallet =
ref.watch(managerProvider.select((value) => value.wallet))
as FiroWallet;
totalBalanceFuture = firoWallet.availablePublicBalance();
availableBalanceFuture = firoWallet.availablePrivateBalance();
} else {
totalBalanceFuture = ref.watch(
managerProvider.select((value) => value.totalBalance));
availableBalanceFuture = ref.watch(managerProvider
.select((value) => value.availableBalance));
}
final locale = ref.watch(localeServiceChangeNotifierProvider
.select((value) => value.locale));
final baseCurrency = ref.watch(prefsChangeNotifierProvider
.select((value) => value.currency));
final priceTuple = ref.watch(priceAnd24hChangeNotifierProvider
.select((value) => value.getPrice(coin)));
final _showAvailable = false;
// ref.watch(walletBalanceToggleStateProvider.state).state ==
// WalletBalanceToggleState.available;
return FutureBuilder(
future: _showAvailable
? availableBalanceFuture
: totalBalanceFuture,
builder: (fbContext, AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData &&
snapshot.data != null) {
if (_showAvailable) {
_balanceCached = snapshot.data!;
} else {
_balanceTotalCached = snapshot.data!;
}
}
Decimal? balanceToShow =
_showAvailable ? _balanceCached : _balanceTotalCached;
if (balanceToShow != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// GestureDetector(
// onTap: showSheet,
// child: Row(
// children: [
// if (coin == Coin.firo ||
// coin == Coin.firoTestNet)
// Text(
// "${_showAvailable ? "Private" : "Public"} Balance",
// style: STextStyles.subtitle500(context)
// .copyWith(
// color: Theme.of(context)
// .extension<StackColors>()!
// .textFavoriteCard,
// ),
// ),
// if (coin != Coin.firo &&
// coin != Coin.firoTestNet)
// Text(
// "${_showAvailable ? "Available" : "Full"} Balance",
// style: STextStyles.subtitle500(context)
// .copyWith(
// color: Theme.of(context)
// .extension<StackColors>()!
// .textFavoriteCard,
// ),
// ),
// const SizedBox(
// width: 4,
// ),
// SvgPicture.asset(
// Assets.svg.chevronDown,
// color: Theme.of(context)
// .extension<StackColors>()!
// .textFavoriteCard,
// width: 8,
// height: 4,
// ),
// ],
// ),
// ),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
"${Format.localizedStringAsFixed(
value: balanceToShow,
locale: locale,
decimalPlaces: 8,
)} ${coin.ticker}",
style: STextStyles.desktopH3(context),
),
),
if (externalCalls)
Text(
"${Format.localizedStringAsFixed(
value: priceTuple.item1 * balanceToShow,
locale: locale,
decimalPlaces: 2,
)} $baseCurrency",
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
);
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// GestureDetector(
// onTap: showSheet,
// child: Row(
// children: [
// if (coin == Coin.firo ||
// coin == Coin.firoTestNet)
// Text(
// "${_showAvailable ? "Private" : "Public"} Balance",
// style: STextStyles.subtitle500(context)
// .copyWith(
// color: Theme.of(context)
// .extension<StackColors>()!
// .textFavoriteCard,
// ),
// ),
// if (coin != Coin.firo &&
// coin != Coin.firoTestNet)
// Text(
// "${_showAvailable ? "Available" : "Full"} Balance",
// style: STextStyles.subtitle500(context)
// .copyWith(
// color: Theme.of(context)
// .extension<StackColors>()!
// .textFavoriteCard,
// ),
// ),
// const SizedBox(
// width: 4,
// ),
// SvgPicture.asset(
// Assets.svg.chevronDown,
// width: 8,
// height: 4,
// color: Theme.of(context)
// .extension<StackColors>()!
// .textFavoriteCard,
// ),
// ],
// ),
// ),
AnimatedText(
stringsToLoopThrough: const [
"Loading balance ",
"Loading balance. ",
"Loading balance.. ",
"Loading balance..."
],
style: STextStyles.desktopH3(context).copyWith(
fontSize: 24,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
if (externalCalls)
AnimatedText(
stringsToLoopThrough: const [
"Loading balance ",
"Loading balance. ",
"Loading balance.. ",
"Loading balance..."
],
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
);
}
},
);
},
),
],
),
const SizedBox(
width: 8,
),
WalletRefreshButton(
walletId: walletId,
initialSyncStatus: widget.initialSyncStatus,
)
],
);
}
}

View file

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
class MyWallet extends StatefulWidget {
const MyWallet({
Key? key,
required this.walletId,
}) : super(key: key);
final String walletId;
@override
State<MyWallet> createState() => _MyWalletState();
}
class _MyWalletState extends State<MyWallet> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return ListView(
primary: false,
children: [
Text(
"My wallet",
style: STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconLeft,
),
),
const SizedBox(
height: 16,
),
Container(
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: BorderRadius.vertical(
top: Radius.circular(
Constants.size.circularBorderRadius,
),
),
),
child: SendReceiveTabMenu(
onChanged: (index) {
setState(() {
_selectedIndex = index;
});
},
),
),
Container(
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(
Constants.size.circularBorderRadius,
),
),
),
child: IndexedStack(
index: _selectedIndex,
children: [
Padding(
key: const Key("desktopSendViewPortKey"),
padding: const EdgeInsets.all(20),
child: DesktopSend(
walletId: widget.walletId,
),
),
Padding(
key: const Key("desktopReceiveViewPortKey"),
padding: const EdgeInsets.all(20),
child: DesktopReceive(
walletId: widget.walletId,
),
),
],
),
),
],
);
}
}

View file

@ -0,0 +1,282 @@
import 'dart:async';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:tuple/tuple.dart';
class NetworkInfoButton extends ConsumerStatefulWidget {
const NetworkInfoButton({
Key? key,
required this.walletId,
this.eventBus,
}) : super(key: key);
final String walletId;
final EventBus? eventBus;
@override
ConsumerState<NetworkInfoButton> createState() => _NetworkInfoButtonState();
}
class _NetworkInfoButtonState extends ConsumerState<NetworkInfoButton> {
late final String walletId;
late final EventBus eventBus;
late WalletSyncStatus _currentSyncStatus;
late NodeConnectionStatus _currentNodeStatus;
late StreamSubscription<dynamic> _syncStatusSubscription;
late StreamSubscription<dynamic> _nodeStatusSubscription;
@override
void initState() {
walletId = widget.walletId;
final managerProvider =
ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId);
eventBus =
widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance;
if (ref.read(managerProvider).isRefreshing) {
_currentSyncStatus = WalletSyncStatus.syncing;
_currentNodeStatus = NodeConnectionStatus.connected;
} else {
_currentSyncStatus = WalletSyncStatus.synced;
if (ref.read(managerProvider).isConnected) {
_currentNodeStatus = NodeConnectionStatus.connected;
} else {
_currentNodeStatus = NodeConnectionStatus.disconnected;
_currentSyncStatus = WalletSyncStatus.unableToSync;
}
}
_syncStatusSubscription =
eventBus.on<WalletSyncStatusChangedEvent>().listen(
(event) async {
if (event.walletId == widget.walletId) {
setState(() {
_currentSyncStatus = event.newStatus;
});
}
},
);
_nodeStatusSubscription =
eventBus.on<NodeConnectionStatusChangedEvent>().listen(
(event) async {
if (event.walletId == widget.walletId) {
setState(() {
_currentNodeStatus = event.newStatus;
});
}
},
);
super.initState();
}
@override
void dispose() {
_nodeStatusSubscription.cancel();
_syncStatusSubscription.cancel();
super.dispose();
}
Widget _buildNetworkIcon(WalletSyncStatus status, BuildContext context) {
const size = 24.0;
switch (status) {
case WalletSyncStatus.unableToSync:
return SvgPicture.asset(
Assets.svg.radioProblem,
color: Theme.of(context).extension<StackColors>()!.accentColorRed,
width: size,
height: size,
);
case WalletSyncStatus.synced:
return SvgPicture.asset(
Assets.svg.radio,
color: Theme.of(context).extension<StackColors>()!.accentColorGreen,
width: size,
height: size,
);
case WalletSyncStatus.syncing:
return SvgPicture.asset(
Assets.svg.radioSyncing,
color: Theme.of(context).extension<StackColors>()!.accentColorYellow,
width: size,
height: size,
);
}
}
Widget _buildText(WalletSyncStatus status, BuildContext context) {
String label;
Color color;
switch (status) {
case WalletSyncStatus.unableToSync:
label = "Unable to sync";
color = Theme.of(context).extension<StackColors>()!.accentColorRed;
break;
case WalletSyncStatus.synced:
label = "Synchronised";
color = Theme.of(context).extension<StackColors>()!.accentColorGreen;
break;
case WalletSyncStatus.syncing:
label = "Synchronising";
color = Theme.of(context).extension<StackColors>()!.accentColorYellow;
break;
}
return Text(
label,
style: STextStyles.desktopMenuItemSelected(context).copyWith(
color: color,
),
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (Util.isDesktop) {
// showDialog<void>(
// context: context,
// builder: (context) => DesktopDialog(
// maxHeight: MediaQuery.of(context).size.height - 64,
// maxWidth: 580,
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// Padding(
// padding: const EdgeInsets.only(
// left: 32,
// ),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// "Network",
// style: STextStyles.desktopH3(context),
// ),
// const DesktopDialogCloseButton(),
// ],
// ),
// ),
// Padding(
// padding: const EdgeInsets.only(
// top: 16,
// left: 32,
// right: 32,
// bottom: 32,
// ),
// child: WalletNetworkSettingsView(
// walletId: walletId,
// initialSyncStatus: _currentSyncStatus,
// initialNodeStatus: _currentNodeStatus,
// ),
// ),
// ],
// ),
// ),
// );
showDialog<void>(
context: context,
builder: (context) => Navigator(
initialRoute: WalletNetworkSettingsView.routeName,
onGenerateRoute: RouteGenerator.generateRoute,
onGenerateInitialRoutes: (_, __) {
return [
FadePageRoute(
DesktopDialog(
maxHeight: MediaQuery.of(context).size.height - 64,
maxWidth: 580,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(
left: 32,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Network",
style: STextStyles.desktopH3(context),
),
DesktopDialogCloseButton(
onPressedOverride: Navigator.of(
context,
rootNavigator: true,
).pop,
),
],
),
),
Padding(
padding: const EdgeInsets.only(
top: 16,
left: 32,
right: 32,
bottom: 32,
),
child: WalletNetworkSettingsView(
walletId: walletId,
initialSyncStatus: _currentSyncStatus,
initialNodeStatus: _currentNodeStatus,
),
),
],
),
),
const RouteSettings(
name: WalletNetworkSettingsView.routeName,
),
),
];
},
),
);
} else {
Navigator.of(context).pushNamed(
WalletNetworkSettingsView.routeName,
arguments: Tuple3(
walletId,
_currentSyncStatus,
_currentNodeStatus,
),
);
}
},
child: Container(
color: Colors.transparent,
child: Row(
children: [
_buildNetworkIcon(_currentSyncStatus, context),
const SizedBox(
width: 6,
),
_buildText(_currentSyncStatus, context),
],
),
),
);
}
}

View file

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
class QRCodeDesktopPopupContent extends StatelessWidget {
const QRCodeDesktopPopupContent({
Key? key,
required this.value,
}) : super(key: key);
final String value;
static const String routeName = "qrCodeDesktopPopupContent";
@override
Widget build(BuildContext context) {
return DesktopDialog(
maxWidth: 614,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: const [
DesktopDialogCloseButton(),
],
),
const SizedBox(
height: 14,
),
QrImage(
data: value,
size: 300,
foregroundColor:
Theme.of(context).extension<StackColors>()!.accentColorDark,
),
],
),
);
}
}

View file

@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
class RecentDesktopTransactions extends ConsumerStatefulWidget {
const RecentDesktopTransactions({
Key? key,
required this.walletId,
}) : super(key: key);
final String walletId;
@override
ConsumerState<RecentDesktopTransactions> createState() =>
_RecentDesktopTransactionsState();
}
class _RecentDesktopTransactionsState
extends ConsumerState<RecentDesktopTransactions> {
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Recent transactions",
style: STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconLeft,
),
),
BlueTextButton(
text: "See all",
onTap: () {
Navigator.of(context).pushNamed(
AllTransactionsView.routeName,
arguments: widget.walletId,
);
},
),
],
),
const SizedBox(
height: 16,
),
Expanded(
child: TransactionsList(
managerProvider: ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManagerProvider(widget.walletId))),
walletId: widget.walletId,
),
),
],
);
}
}

View file

@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
class SendReceiveTabMenu extends StatefulWidget {
const SendReceiveTabMenu({
Key? key,
this.initialIndex = 0,
this.onChanged,
}) : super(key: key);
final int initialIndex;
final void Function(int)? onChanged;
@override
State<SendReceiveTabMenu> createState() => _SendReceiveTabMenuState();
}
class _SendReceiveTabMenuState extends State<SendReceiveTabMenu> {
late int _selectedIndex;
void _onChanged(int newIndex) {
if (_selectedIndex != newIndex) {
setState(() {
_selectedIndex = newIndex;
});
widget.onChanged?.call(_selectedIndex);
}
}
@override
void initState() {
_selectedIndex = widget.initialIndex;
super.initState();
}
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => _onChanged(0),
child: Container(
color: Colors.transparent,
child: Column(
children: [
const SizedBox(
height: 16,
),
Text(
"Send",
style: STextStyles.desktopTextExtraSmall(context).copyWith(
color: _selectedIndex == 0
? Theme.of(context)
.extension<StackColors>()!
.accentColorBlue
: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
const SizedBox(
height: 19,
),
Container(
height: 2,
decoration: BoxDecoration(
color: _selectedIndex == 0
? Theme.of(context)
.extension<StackColors>()!
.accentColorBlue
: Theme.of(context)
.extension<StackColors>()!
.background,
),
),
],
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () => _onChanged(1),
child: Container(
color: Colors.transparent,
child: Column(
children: [
const SizedBox(
height: 16,
),
Text(
"Receive",
style: STextStyles.desktopTextExtraSmall(context).copyWith(
color: _selectedIndex == 1
? Theme.of(context)
.extension<StackColors>()!
.accentColorBlue
: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
const SizedBox(
height: 19,
),
Container(
height: 2,
decoration: BoxDecoration(
color: _selectedIndex == 1
? Theme.of(context)
.extension<StackColors>()!
.accentColorBlue
: Theme.of(context)
.extension<StackColors>()!
.background,
),
),
],
),
),
),
),
],
);
}
}

View file

@ -0,0 +1,243 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
class UnlockWalletKeysDesktop extends ConsumerStatefulWidget {
const UnlockWalletKeysDesktop({
Key? key,
required this.walletId,
}) : super(key: key);
final String walletId;
static const String routeName = "/desktopUnlockWalletKeys";
@override
ConsumerState<UnlockWalletKeysDesktop> createState() =>
_UnlockWalletKeysDesktopState();
}
class _UnlockWalletKeysDesktopState
extends ConsumerState<UnlockWalletKeysDesktop> {
late final TextEditingController passwordController;
late final FocusNode passwordFocusNode;
bool continueEnabled = false;
bool hidePassword = true;
@override
void initState() {
passwordController = TextEditingController();
passwordFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
passwordController.dispose();
passwordFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return DesktopDialog(
maxWidth: 579,
maxHeight: double.infinity,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
DesktopDialogCloseButton(
onPressedOverride: Navigator.of(
context,
rootNavigator: true,
).pop,
),
],
),
const SizedBox(
height: 12,
),
SvgPicture.asset(
Assets.svg.keys,
width: 100,
height: 58,
),
const SizedBox(
height: 55,
),
Text(
"Wallet keys",
style: STextStyles.desktopH2(context),
),
const SizedBox(
height: 16,
),
Text(
"Enter your password",
style: STextStyles.desktopTextMedium(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
),
const SizedBox(
height: 24,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("enterPasswordUnlockWalletKeysDesktopFieldKey"),
focusNode: passwordFocusNode,
controller: passwordController,
style: STextStyles.desktopTextMedium(context).copyWith(
height: 2,
),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Enter password",
passwordFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: SizedBox(
height: 70,
child: Row(
children: [
GestureDetector(
key: const Key(
"enterUnlockWalletKeysDesktopFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: Container(
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(1000),
),
height: 32,
width: 32,
child: Center(
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 24,
height: 19,
),
),
),
),
const SizedBox(
width: 10,
),
],
),
),
),
),
onChanged: (newValue) {
setState(() {
continueEnabled = newValue.isNotEmpty;
});
},
),
),
),
const SizedBox(
height: 55,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
onPressed: Navigator.of(
context,
rootNavigator: true,
).pop,
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
label: "Continue",
enabled: continueEnabled,
onPressed: continueEnabled
? () async {
// todo: check password
// Navigator.of(context).pop();
final words = await ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.mnemonic;
await Navigator.of(context).pushReplacementNamed(
WalletKeysDesktopPopup.routeName,
arguments: words,
);
//
// await showDialog<void>(
// context: context,
// barrierDismissible: false,
// builder: (context) => Navigator(
// initialRoute: WalletKeysDesktopPopup.routeName,
// onGenerateRoute: RouteGenerator.generateRoute,
// onGenerateInitialRoutes: (_, __) {
// return [
// RouteGenerator.generateRoute(
// RouteSettings(
// name: WalletKeysDesktopPopup.routeName,
// arguments: words,
// ),
// )
// ];
// },
// ),
// );
}
: null,
),
),
],
),
),
const SizedBox(
height: 32,
),
],
),
);
}
}

View file

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
class WalletKeysButton extends StatelessWidget {
const WalletKeysButton({
Key? key,
required this.walletId,
}) : super(key: key);
final String walletId;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) => Navigator(
initialRoute: UnlockWalletKeysDesktop.routeName,
onGenerateRoute: RouteGenerator.generateRoute,
onGenerateInitialRoutes: (_, __) {
return [
RouteGenerator.generateRoute(
RouteSettings(
name: UnlockWalletKeysDesktop.routeName,
arguments: walletId,
),
)
];
},
),
);
// showDialog<void>(
// context: context,
// barrierDismissible: false,
// builder: (context) => UnlockWalletKeysDesktop(
// walletId: walletId,
// ),
// );
},
child: Container(
color: Colors.transparent,
child: Row(
children: [
SvgPicture.asset(
Assets.svg.key,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
const SizedBox(
width: 6,
),
Text(
"Wallet keys",
style: STextStyles.desktopMenuItemSelected(context),
)
],
),
),
);
}
}

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